Merge branch 'master' into 41120-performance-bar-auto-scroll

* master: (120 commits)
  Update CHANGELOG.md for 10.3.3
  Fix user membership destroy relation
  Use heredoc so it's more clear
  Put all menus under menu module
  Introduce common project settings and just put
  Introduce Factory::Resource::DeployKey
  Introduce expand_deploy_keys
  Introduce Menu::Side
  Add documents for GitLab utilities
  Clears visual token on second backspace
  Update prometheus gem to version that adds inf+ bucket in accordance with Prometheus docs.
  Add breadcrumbs to User Settings sub-views
  Rename asset sync related AWS variables
  Allow logged in user to change his password
  Fix 404 error after a user edits an issue description and solves the reCAPTCHA
  Fix links to old commits in merge requests
  Typos correction on the advise of @smcgivern
  Documenting that resolved JIRA tickets are not automatically transitioned
  Forking a project to a namespace with lower visibility.
  Keep typographic hierarchy in User Settings
  ...
This commit is contained in:
Filipa Lacerda 2018-01-02 21:06:06 +00:00
commit 6db6856d8d
960 changed files with 9080 additions and 7731 deletions

View file

@ -16,6 +16,7 @@ engines:
enabled: true
rubocop:
enabled: true
channel: "gitlab-rubocop-0-52"
ratings:
paths:
- Gemfile.lock

View file

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
@ -594,12 +594,20 @@ codequality:
script:
- cp .rubocop.yml .rubocop.yml.bak
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-v2 analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- mv .rubocop.yml.bak .rubocop.yml
artifacts:
paths: [codeclimate.json]
sast:
image: registry.gitlab.com/gitlab-org/gl-sast:latest
before_script: []
script:
- /app/bin/run .
artifacts:
paths: [gl-sast-report.json]
qa:internal:
<<: *dedicated-runner
<<: *except-docs

File diff suppressed because it is too large Load diff

View file

@ -1,53 +1,111 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
# on 2017-07-10 01:48:30 +0900 using RuboCop version 0.49.1.
# `rubocop --auto-gen-config`
# on 2017-12-14 12:04:26 +0100 using RuboCop version 0.52.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 181
# Offense count: 174
Capybara/CurrentPathExpectation:
Enabled: false
# Offense count: 951
Capybara/FeatureMethods:
Enabled: false
# Offense count: 24
FactoryBot/DynamicAttributeDefinedStatically:
Exclude:
- 'spec/factories/broadcast_messages.rb'
- 'spec/factories/ci/builds.rb'
- 'spec/factories/ci/runners.rb'
- 'spec/factories/clusters/applications/helm.rb'
- 'spec/factories/clusters/applications/ingress.rb'
- 'spec/factories/clusters/platforms/kubernetes.rb'
- 'spec/factories/emails.rb'
- 'spec/factories/gpg_keys.rb'
- 'spec/factories/group_members.rb'
- 'spec/factories/merge_requests.rb'
- 'spec/factories/notes.rb'
- 'spec/factories/oauth_access_grants.rb'
- 'spec/factories/project_members.rb'
- 'spec/factories/todos.rb'
- 'spec/factories/uploads.rb'
# Offense count: 65
# Cop supports --auto-correct.
Layout/EmptyLinesAroundArguments:
Enabled: false
# Offense count: 249
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Enabled: false
# Offense count: 119
# Offense count: 82
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
Layout/IndentArray:
Enabled: false
# Offense count: 208
# Offense count: 239
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
Layout/IndentHash:
Enabled: false
# Offense count: 8
# Offense count: 15
# Cop supports --auto-correct.
# Configuration parameters: .
# SupportedStyles: space, no_space
# SupportedStylesForEmptyBraces: space, no_space
Layout/SpaceBeforeBlockBraces:
EnforcedStyle: space
EnforcedStyleForEmptyBraces: space
# Offense count: 11
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceBeforeFirstArg:
Enabled: false
Exclude:
- 'config/routes/project.rb'
- 'db/migrate/20170506185517_add_foreign_key_pipeline_schedules_and_pipelines.rb'
- 'features/steps/project/source/browse_files.rb'
- 'features/steps/project/source/markdown_render.rb'
- 'lib/api/runners.rb'
- 'spec/features/search/user_uses_search_filters_spec.rb'
- 'spec/routing/project_routing_spec.rb'
- 'spec/services/system_note_service_spec.rb'
# Offense count: 64
# Offense count: 93
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_no_space, require_space
Layout/SpaceInLambdaLiteral:
Enabled: false
# Offense count: 256
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
# SupportedStyles: space, no_space, compact
# SupportedStylesForEmptyBrackets: space, no_space
Layout/SpaceInsideArrayLiteralBrackets:
Exclude:
- 'spec/lib/gitlab/import_export/relation_factory_spec.rb'
# Offense count: 323
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
# SupportedStylesForEmptyBraces: space, no_space
Layout/SpaceInsideBlockBraces:
Enabled: false
# Offense count: 135
# Offense count: 146
# Cop supports --auto-correct.
Layout/SpaceInsideParens:
Enabled: false
@ -55,178 +113,535 @@ Layout/SpaceInsideParens:
# Offense count: 14
# Cop supports --auto-correct.
Layout/SpaceInsidePercentLiteralDelimiters:
Enabled: false
Exclude:
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/health_checks/fs_shards_check.rb'
- 'spec/lib/gitlab/health_checks/fs_shards_check_spec.rb'
# Offense count: 272
RSpec/EmptyLineAfterFinalLet:
Enabled: false
# Offense count: 181
RSpec/EmptyLineAfterSubject:
Enabled: false
# Offense count: 9
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: it_behaves_like, it_should_behave_like
RSpec/ItBehavesLike:
Enabled: false
# Offense count: 25
Lint/DuplicateMethods:
Exclude:
- 'app/models/application_setting.rb'
- 'app/models/commit.rb'
- 'app/models/note.rb'
- 'app/services/merge_requests/merge_service.rb'
- 'lib/bitbucket/representation/repo.rb'
- 'lib/declarative_policy/base.rb'
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- 'lib/gitlab/cycle_analytics/base_event_fetcher.rb'
- 'lib/gitlab/diff/formatters/base_formatter.rb'
- 'lib/gitlab/git/blob.rb'
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/tree.rb'
- 'lib/gitlab/git/wiki_page.rb'
- 'lib/gitlab/ldap/person.rb'
- 'lib/gitlab/o_auth/user.rb'
# Offense count: 4
RSpec/IteratedExpectation:
Lint/InterpolationCheck:
Exclude:
- 'spec/features/issues/filtered_search/filter_issues_spec.rb'
- 'spec/features/users_spec.rb'
- 'spec/services/quick_actions/interpret_service_spec.rb'
# Offense count: 198
# Configuration parameters: MaximumRangeSize.
Lint/MissingCopEnableDirective:
Enabled: false
# Offense count: 2
RSpec/OverwritingSetup:
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
Exclude:
- 'app/models/project.rb'
# Offense count: 1
# Configuration parameters: IgnoreImplicitReferences.
Lint/ShadowedArgument:
Exclude:
- 'lib/gitlab/database/sha_attribute.rb'
# Offense count: 3
# Cop supports --auto-correct.
Lint/UnneededRequireStatement:
Exclude:
- 'db/post_migrate/20161221153951_rename_reserved_project_names.rb'
- 'db/post_migrate/20170313133418_rename_more_reserved_project_names.rb'
- 'lib/declarative_policy.rb'
# Offense count: 9
Lint/UriEscapeUnescape:
Exclude:
- 'app/controllers/application_controller.rb'
- 'app/models/project_services/drone_ci_service.rb'
- 'spec/lib/google_api/auth_spec.rb'
- 'spec/requests/api/files_spec.rb'
- 'spec/requests/api/internal_spec.rb'
- 'spec/requests/api/issues_spec.rb'
- 'spec/requests/api/v3/issues_spec.rb'
# Offense count: 2
Naming/ConstantName:
Exclude:
- 'lib/gitlab/import_sources.rb'
- 'lib/gitlab/ssh_public_key.rb'
# Offense count: 11
# Configuration parameters: EnforcedStyle.
# SupportedStyles: lowercase, uppercase
Naming/HeredocDelimiterCase:
Exclude:
- 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb'
- 'spec/support/seed_repo.rb'
# Offense count: 101
# Configuration parameters: Blacklist.
# Blacklist: END, (?-mix:EO[A-Z]{1})
Naming/HeredocDelimiterNaming:
Enabled: false
# Offense count: 36
RSpec/RepeatedExample:
Enabled: false
# Offense count: 86
RSpec/ScatteredLet:
Enabled: false
# Offense count: 20
RSpec/ScatteredSetup:
# Offense count: 28
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect.
Performance/HashEachMethods:
Enabled: false
# Offense count: 1
RSpec/SharedContext:
Performance/UnfreezeString:
Exclude:
- 'features/steps/project/commits/commits.rb'
# Offense count: 1
# Cop supports --auto-correct.
Performance/UriDefaultParser:
Exclude:
- 'lib/gitlab/url_sanitizer.rb'
# Offense count: 3745
# Configuration parameters: Prefixes.
# Prefixes: when, with, without
RSpec/ContextWording:
Enabled: false
# Offense count: 115
# Offense count: 291
RSpec/EmptyLineAfterFinalLet:
Enabled: false
# Offense count: 180
RSpec/EmptyLineAfterSubject:
Enabled: false
# Offense count: 220
RSpec/ExpectInHook:
Enabled: false
# Offense count: 7
# Configuration parameters: EnforcedStyle.
# SupportedStyles: implicit, each, example
RSpec/HookArgument:
Exclude:
- 'spec/spec_helper.rb'
- 'spec/support/carrierwave.rb'
- 'spec/support/db_cleaner.rb'
- 'spec/support/gitaly.rb'
- 'spec/support/setup_builds_storage.rb'
# Offense count: 19
# Configuration parameters: EnforcedStyle.
# SupportedStyles: it_behaves_like, it_should_behave_like
RSpec/ItBehavesLike:
Exclude:
- 'spec/lib/gitlab/git/commit_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- 'spec/lib/gitlab/shell_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/workers/git_garbage_collect_worker_spec.rb'
# Offense count: 5
RSpec/IteratedExpectation:
Exclude:
- 'spec/features/admin/admin_settings_spec.rb'
- 'spec/features/merge_requests/diff_notes_resolve_spec.rb'
- 'spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb'
- 'spec/lib/gitlab/gitlab_import/client_spec.rb'
- 'spec/lib/gitlab/legacy_github_import/client_spec.rb'
# Offense count: 75
RSpec/LetBeforeExamples:
Exclude:
- 'spec/controllers/projects/commit_controller_spec.rb'
- 'spec/lib/banzai/filter/issue_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/user_reference_filter_spec.rb'
- 'spec/lib/gitlab/email/handler/create_issue_handler_spec.rb'
- 'spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb'
- 'spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
- 'spec/models/commit_range_spec.rb'
- 'spec/models/milestone_spec.rb'
- 'spec/models/project_services/packagist_service_spec.rb'
- 'spec/models/repository_spec.rb'
- 'spec/rubocop/cop/migration/update_column_in_batches_spec.rb'
- 'spec/serializers/pipeline_details_entity_spec.rb'
- 'spec/views/ci/lints/show.html.haml_spec.rb'
# Offense count: 1
RSpec/MultipleSubjects:
Exclude:
- 'spec/services/merge_requests/create_from_issue_service_spec.rb'
# Offense count: 4
RSpec/OverwritingSetup:
Exclude:
- 'spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb'
- 'spec/models/email_spec.rb'
- 'spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'
- 'spec/services/notes/quick_actions_service_spec.rb'
# Offense count: 917
# Configuration parameters: Strict, EnforcedStyle.
# SupportedStyles: inflected, explicit
RSpec/PredicateMatcher:
Enabled: false
# Offense count: 35
RSpec/RepeatedExample:
Enabled: false
# Offense count: 132
# Configuration parameters: EnforcedStyle.
# SupportedStyles: and_return, block
RSpec/ReturnFromStub:
Enabled: false
# Offense count: 105
RSpec/ScatteredLet:
Enabled: false
# Offense count: 22
RSpec/ScatteredSetup:
Exclude:
- 'spec/controllers/projects/templates_controller_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- 'spec/lib/gitlab/git/env_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- 'spec/requests/api/v3/builds_spec.rb'
- 'spec/requests/api/v3/projects_spec.rb'
- 'spec/services/projects/create_service_spec.rb'
# Offense count: 1
RSpec/SharedContext:
Exclude:
- 'spec/features/admin/admin_groups_spec.rb'
# Offense count: 90
RSpec/SingleLineHook:
Enabled: false
# Offense count: 5
RSpec/VoidExpect:
Exclude:
- 'spec/features/projects/artifacts/download_spec.rb'
- 'spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb'
- 'spec/models/ci/group_spec.rb'
- 'spec/models/ci/runner_spec.rb'
- 'spec/services/users/destroy_service_spec.rb'
# Offense count: 40
# Configuration parameters: Include.
# Include: db/migrate/*.rb
Rails/CreateTableWithTimestamps:
Enabled: false
# Offense count: 149
Rails/FilePath:
Enabled: false
# Offense count: 119
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasManyOrHasOneDependent:
Enabled: false
# Offense count: 113
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/InverseOf:
Enabled: false
# Offense count: 48
# Configuration parameters: Include.
# Include: app/controllers/**/*.rb
Rails/LexicallyScopedActionFilter:
Enabled: false
# Offense count: 14
# Cop supports --auto-correct.
Rails/Presence:
Exclude:
- 'app/controllers/projects/blob_controller.rb'
- 'app/models/ci/pipeline.rb'
- 'app/models/clusters/platforms/kubernetes.rb'
- 'app/models/concerns/mentionable.rb'
- 'app/models/concerns/token_authenticatable.rb'
- 'app/models/project_services/hipchat_service.rb'
- 'app/models/project_services/irker_service.rb'
- 'app/models/project_services/jira_service.rb'
- 'app/models/project_services/kubernetes_service.rb'
- 'app/models/project_services/packagist_service.rb'
- 'app/models/wiki_page.rb'
- 'lib/gitlab/git/hook.rb'
- 'lib/gitlab/github_import/importer/releases_importer.rb'
# Offense count: 14
# Cop supports --auto-correct.
Rails/RedundantReceiverInWithOptions:
Exclude:
- 'config/initializers/doorkeeper_openid_connect.rb'
# Offense count: 2
# Configuration parameters: Include.
# Include: db/migrate/*.rb
Rails/ReversibleMigration:
Enabled: false
Exclude:
- 'db/migrate/20160824103857_drop_unused_ci_tables.rb'
# Offense count: 336
# Offense count: 430
# Configuration parameters: Blacklist.
# Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
Rails/SkipsModelValidations:
Enabled: false
# Offense count: 11
# Offense count: 1
# Configuration parameters: Environments.
# Environments: development, test, production
Rails/UnknownEnv:
Exclude:
- 'db/migrate/20171124125748_populate_missing_merge_request_statuses.rb'
# Offense count: 13
# Cop supports --auto-correct.
Security/YAMLLoad:
Enabled: false
Exclude:
- 'config/initializers/carrierwave.rb'
- 'lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb'
- 'lib/gitlab/redis/wrapper.rb'
- 'lib/system_check/incoming_email/imap_authentication_check.rb'
- 'spec/config/mail_room_spec.rb'
- 'spec/initializers/secret_token_spec.rb'
- 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- 'spec/models/clusters/platforms/kubernetes_spec.rb'
- 'spec/models/project_services/kubernetes_service_spec.rb'
# Offense count: 58
# Offense count: 63
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
# Offense count: 6
# Cop supports --auto-correct.
Style/EachWithObject:
# Offense count: 5
Style/CommentedKeyword:
Exclude:
- 'lib/tasks/gitlab/backup.rake'
- 'spec/tasks/gitlab/backup_rake_spec.rb'
# Offense count: 30
Style/DateTime:
Enabled: false
# Offense count: 31
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/Dir:
Exclude:
- 'qa/qa.rb'
# Offense count: 9
# Cop supports --auto-correct.
Style/EachWithObject:
Exclude:
- 'config/initializers/gollum.rb'
- 'lib/expand_variables.rb'
- 'lib/gitlab/ci/ansi2html.rb'
- 'lib/gitlab/ee_compat_check.rb'
- 'lib/gitlab/hook_data/issuable_builder.rb'
- 'lib/gitlab/i18n/po_linter.rb'
- 'lib/gitlab/import_export/members_mapper.rb'
- 'lib/gitlab/import_export/relation_factory.rb'
- 'scripts/static-analysis'
# Offense count: 24
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: empty, nil, both
Style/EmptyElse:
Enabled: false
# Offense count: 9
# Offense count: 14
# Cop supports --auto-correct.
Style/EmptyLambdaParameter:
Exclude:
- 'app/models/ci/build.rb'
- 'app/models/ci/runner.rb'
# Offense count: 12
# Cop supports --auto-correct.
Style/EmptyLiteral:
Enabled: false
Exclude:
- 'features/steps/project/commits/commits.rb'
- 'lib/gitlab/fogbugz_import/importer.rb'
- 'lib/gitlab/git/diff_collection.rb'
- 'lib/gitlab/gitaly_client.rb'
- 'scripts/trigger-build-omnibus'
- 'spec/features/merge_requests/versions_spec.rb'
- 'spec/helpers/merge_requests_helper_spec.rb'
- 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- 'spec/support/chat_slash_commands_shared_examples.rb'
# Offense count: 78
# Offense count: 98
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Enabled: false
# Offense count: 23
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/Encoding:
Enabled: false
# Offense count: 2
Style/EvalWithLocation:
Exclude:
- 'app/models/service.rb'
# Offense count: 52
# Cop supports --auto-correct.
# Configuration parameters: Autocorrect, EnforcedStyle.
# SupportedStyles: module_function, extend_self
Style/ExtendSelf:
Enabled: false
# Offense count: 34
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: format, sprintf, percent
Style/FormatString:
Enabled: false
# Offense count: 301
# Offense count: 371
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
# Offense count: 18
# Offense count: 21
Style/IfInsideElse:
Enabled: false
# Offense count: 182
# Offense count: 781
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
Enabled: false
# Offense count: 52
# Offense count: 71
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: line_count_dependent, lambda, literal
Style/Lambda:
Enabled: false
# Offense count: 6
# Offense count: 11
# Cop supports --auto-correct.
Style/LineEndConcatenation:
Enabled: false
Exclude:
- 'app/helpers/tree_helper.rb'
- 'spec/features/issuables/markdown_references_spec.rb'
- 'spec/lib/gitlab/checks/project_moved_spec.rb'
- 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
- 'spec/lib/gitlab/incoming_email_spec.rb'
# Offense count: 40
# Offense count: 39
# Cop supports --auto-correct.
Style/MethodCallWithoutArgsParentheses:
Enabled: false
# Offense count: 13
# Offense count: 17
Style/MethodMissing:
Enabled: false
# Offense count: 7
Style/MixinUsage:
Exclude:
- 'features/support/env.rb'
- 'spec/factories/ci/builds.rb'
- 'spec/factories/ci/job_artifacts.rb'
- 'spec/factories/lfs_objects.rb'
- 'spec/factories/notes.rb'
- 'spec/lib/gitlab/import_export/project_tree_restorer_spec.rb'
- 'spec/lib/gitlab/import_export/version_checker_spec.rb'
# Offense count: 6
# Cop supports --auto-correct.
Style/MultilineIfModifier:
Enabled: false
Exclude:
- 'app/helpers/snippets_helper.rb'
- 'app/models/project_wiki.rb'
- 'app/services/ci/process_pipeline_service.rb'
- 'app/services/create_deployment_service.rb'
- 'lib/api/commit_statuses.rb'
- 'lib/gitlab/ci/trace.rb'
# Offense count: 26
# Offense count: 23
# Cop supports --auto-correct.
# Configuration parameters: Whitelist.
# Whitelist: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with
Style/NestedParenthesizedCalls:
Enabled: false
# Offense count: 20
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# Configuration parameters: EnforcedStyle, MinBodyLength.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Enabled: false
# Offense count: 45
# Offense count: 58
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# Configuration parameters: EnforcedOctalStyle.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
# Offense count: 98
# Offense count: 112
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
# Configuration parameters: AutoCorrect, EnforcedStyle.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Enabled: false
# Offense count: 42
# Offense count: 4
# Cop supports --auto-correct.
Style/OrAssignment:
Exclude:
- 'app/models/concerns/token_authenticatable.rb'
- 'lib/api/commit_statuses.rb'
- 'lib/api/v3/members.rb'
- 'lib/gitlab/project_transfer.rb'
# Offense count: 50
# Cop supports --auto-correct.
Style/ParallelAssignment:
Enabled: false
# Offense count: 800
# Offense count: 891
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@ -235,106 +650,194 @@ Style/PercentLiteralDelimiters:
# Offense count: 15
# Cop supports --auto-correct.
Style/PerlBackrefs:
Enabled: false
Exclude:
- 'app/controllers/projects/application_controller.rb'
- 'app/helpers/submodule_helper.rb'
- 'lib/backup/manager.rb'
- 'lib/banzai/filter/abstract_reference_filter.rb'
- 'lib/banzai/filter/autolink_filter.rb'
- 'lib/banzai/filter/emoji_filter.rb'
- 'lib/banzai/filter/gollum_tags_filter.rb'
- 'lib/expand_variables.rb'
- 'lib/gitlab/diff/highlight.rb'
- 'lib/gitlab/search_results.rb'
- 'lib/gitlab/sherlock/query.rb'
# Offense count: 58
# Offense count: 82
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, exploded
Style/RaiseArgs:
Enabled: false
# Offense count: 6
# Offense count: 8
# Cop supports --auto-correct.
Style/RedundantBegin:
Enabled: false
Exclude:
- 'app/controllers/projects/clusters/gcp_controller.rb'
- 'app/models/merge_request.rb'
- 'app/services/projects/import_service.rb'
- 'lib/api/branches.rb'
- 'lib/gitlab/current_settings.rb'
- 'lib/gitlab/git/commit.rb'
- 'lib/gitlab/health_checks/base_abstract_check.rb'
- 'lib/tasks/gitlab/task_helpers.rb'
# Offense count: 37
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantConditional:
Exclude:
- 'lib/system_check/helpers.rb'
# Offense count: 58
# Cop supports --auto-correct.
Style/RedundantFreeze:
Enabled: false
# Offense count: 14
# Offense count: 15
# Cop supports --auto-correct.
# Configuration parameters: AllowMultipleReturnValues.
Style/RedundantReturn:
Enabled: false
Exclude:
- 'app/controllers/application_controller.rb'
- 'app/controllers/concerns/issuable_actions.rb'
- 'app/controllers/groups/application_controller.rb'
- 'app/controllers/omniauth_callbacks_controller.rb'
- 'app/controllers/profiles/keys_controller.rb'
- 'app/controllers/projects/application_controller.rb'
- 'app/services/access_token_validation_service.rb'
- 'lib/gitlab/utils.rb'
- 'lib/google_api/auth.rb'
# Offense count: 406
# Offense count: 454
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
# Offense count: 115
# Offense count: 140
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Enabled: false
# Offense count: 29
# Offense count: 35
# Cop supports --auto-correct.
Style/RescueModifier:
Enabled: false
# Offense count: 105
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: implicit, explicit
Style/RescueStandardError:
Enabled: false
# Offense count: 91
# Cop supports --auto-correct.
# Configuration parameters: ConvertCodeThatCanStartToReturnNil.
Style/SafeNavigation:
Enabled: false
# Offense count: 8
# Cop supports --auto-correct.
Style/SelfAssignment:
Enabled: false
Exclude:
- 'app/models/concerns/bulk_member_access_load.rb'
- 'app/serializers/base_serializer.rb'
- 'app/services/notification_service.rb'
- 'lib/api/runners.rb'
- 'spec/features/merge_requests/diff_notes_resolve_spec.rb'
- 'spec/features/projects/clusters/interchangeability_spec.rb'
- 'spec/support/import_export/configuration_helper.rb'
# Offense count: 50
# Cop supports --auto-correct.
# Configuration parameters: AllowIfMethodIsEmpty.
Style/SingleLineMethods:
Enabled: false
Exclude:
- 'lib/gitlab/ci/ansi2html.rb'
# Offense count: 64
# Offense count: 66
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# Configuration parameters: .
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names
# Offense count: 44
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/StderrPuts:
Exclude:
- 'config/initializers/rspec_profiling.rb'
# Offense count: 45
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
# Offense count: 84
# Offense count: 99
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
Style/SymbolProc:
Enabled: false
# Offense count: 8
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Enabled: false
Exclude:
- 'app/finders/projects_finder.rb'
- 'app/helpers/namespaces_helper.rb'
- 'features/support/capybara.rb'
- 'lib/api/v3/projects.rb'
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- 'spec/requests/api/pipeline_schedules_spec.rb'
- 'spec/support/capybara.rb'
# Offense count: 17
# Cop supports --auto-correct.
# Configuration parameters: AllowNamedUnderscoreVariables.
Style/TrailingUnderscoreVariable:
Enabled: false
Exclude:
- 'app/controllers/admin/background_jobs_controller.rb'
- 'app/controllers/invites_controller.rb'
- 'app/helpers/tab_helper.rb'
- 'lib/backup/manager.rb'
- 'lib/gitlab/logger.rb'
- 'lib/gitlab/upgrader.rb'
- 'lib/system_check/app/migrations_are_up_check.rb'
- 'lib/system_check/incoming_email/mail_room_running_check.rb'
- 'lib/tasks/gitlab/check.rake'
- 'lib/tasks/gitlab/task_helpers.rb'
- 'spec/lib/gitlab/etag_caching/middleware_spec.rb'
- 'spec/services/quick_actions/interpret_service_spec.rb'
# Offense count: 4
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist.
# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym
Style/TrivialAccessors:
Enabled: false
Exclude:
- 'app/models/external_issue.rb'
- 'app/serializers/base_serializer.rb'
- 'lib/gitlab/ldap/person.rb'
- 'lib/system_check/base_check.rb'
# Offense count: 5
# Offense count: 4
# Cop supports --auto-correct.
Style/UnlessElse:
Enabled: false
Exclude:
- 'lib/backup/manager.rb'
- 'lib/gitlab/project_search_results.rb'
- 'lib/tasks/gitlab/check.rake'
- 'spec/features/issues/award_emoji_spec.rb'
# Offense count: 28
# Offense count: 30
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Enabled: false
@ -342,4 +845,19 @@ Style/UnneededInterpolation:
# Offense count: 11
# Cop supports --auto-correct.
Style/ZeroLengthPredicate:
Enabled: false
Exclude:
- 'app/models/deploy_key.rb'
- 'app/models/network/commit.rb'
- 'app/models/network/graph.rb'
- 'app/models/project_services/asana_service.rb'
- 'app/services/boards/create_service.rb'
- 'app/services/merge_requests/conflicts/list_service.rb'
- 'lib/declarative_policy/dsl.rb'
- 'lib/extracts_path.rb'
- 'lib/gitlab/git/repository.rb'
# Offense count: 22050
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 1310

View file

@ -1 +1 @@
2.3.5
2.3.6

View file

@ -2,6 +2,184 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.3.3 (2018-01-02)
### Fixed (3 changes)
- Fix links to old commits in merge request comments.
- Fix 404 errors after a user edits an issue description and solves the reCAPTCHA.
- Gracefully handle orphaned write deploy keys in /internal/post_receive.
## 10.3.2 (2017-12-28)
### Fixed (1 change)
- Fix migration for removing orphaned issues.moved_to_id values in MySQL and PostgreSQL.
## 10.3.1 (2017-12-27)
### Fixed (3 changes)
- Don't link LFS objects to a project when unlinking forks when they were already linked. !16006
- Execute project hooks and services after commit when moving an issue.
- Fix Error 500s with anonymous clones for a project that has moved.
### Changed (1 change)
- Reduce the number of buckets in gitlab_cache_operation_duration_seconds metric. !15881
## 10.3.0 (2017-12-22)
### Security (1 change, 1 of them is from the community)
- Upgrade jQuery to 2.2.4. !15570 (Takuya Noguchi)
### Fixed (55 changes, 8 of them are from the community)
- Fail jobs if its dependency is missing. !14009
- Fix errors when selecting numeric-only labels in the labels autocomplete selector. !14607 (haseebeqx)
- Fix pipeline status transition for single manual job. This would also fix pipeline duration becuse it is depending on status transition. !15251
- Fix acceptance of username for Mattermost service update. !15275
- Set the default gitlab-shell timeout to 3 hours. !15292
- Make sure a user can add projects to subgroups they have access to. !15294
- OAuth identity lookups case-insensitive. !15312
- Fix filter by my reaction is not working. !15345 (Hiroyuki Sato)
- Avoid deactivation when pipeline schedules execute a branch includes `[ci skip]` comment. !15405
- Add recaptcha modal to issue updates detected as spam. !15408
- Fix item name and namespace text overflow in Projects dropdown. !15451
- Removed unused rake task, 'rake gitlab:sidekiq:drop_post_receive'. !15493
- Fix commits page throwing 500 when the multi-file editor was enabled. !15502
- Fix Issue comment submit button being disabled when pasting content from another GFM note. !15530
- Reenable Prometheus metrics, add more control over Prometheus method instrumentation. !15558
- Fix broadcast message not showing up on login page. !15578
- Initializes the branches dropdown when the 'Start new pipeline' failed due to validation errors. !15588 (Christiaan Van den Poel)
- Fix merge requests where the source or target branch name matches a tag name. !15591
- Create a fork network for forks with a deleted source. !15595
- Fix search results when a filename would contain a special character. !15606 (haseebeqx)
- Strip leading & trailing whitespaces in CI/CD secret variable keys. !15615
- Correctly link to a forked project from the new fork page. !15653
- Fix the fork project functionality for projects with hashed storage. !15671
- Added default order to UsersFinder. !15679
- Fix graph notes number duplication. !15696 (Vladislav Kaverin)
- Fix updateEndpoint undefined error for issue_show app root. !15698
- Change boards page boards_data absolute urls to paths. !15703
- Using appropiate services in the API for managing forks. !15709
- Confirming email with invalid token should no longer generate an error. !15726
- fix #39233 - 500 in merge request. !15774 (Martin Nowak)
- Use Markdown styling for new project guidelines. !15785 (Markus Koller)
- Fix error during schema dump. !15866
- Fix broken illustration images for monitoring page empty states. !15889
- Make sure user email is read only when synced with LDAP. !15915
- Fixed outdated browser flash positioning.
- Fix gitlab:import:repos Rake task moving repositories into the wrong location.
- Gracefully handle case when repository's root ref does not exist.
- Fix GitHub importer using removed interface.
- Align retry button with job title with new grid size.
- Fixed admin welcome screen new group path.
- Fix related branches/Merge requests failing to load when the hostname setting is changed.
- Init zen mode in snippets pages.
- Remove extra margin from wordmark in header.
- Fixed long commit links not wrapping correctly.
- Fixed deploy keys remove button loading state not resetting.
- Use app host instead of asset host when rendering image blob or diff.
- Hide log size for mobile screens.
- Fix sending notification emails to users with the mention level set who were mentioned in an issue or merge request description.
- Changed validation error message on wrong milestone dates. (Xurxo Méndez Pérez)
- Fix access to the final page of todos.
- Fixed new group milestone breadcrumbs.
- Fix image diff notification email from showing wrong content.
- Fixed merge request lock icon size.
- Make sure head pippeline always corresponds with the head sha of an MR.
- Prevent 500 error when inspecting job after trigger was removed.
### Changed (14 changes, 2 of them are from the community)
- Only owner or master can erase jobs. !15216
- Allow password authentication to be disabled entirely. !15223 (Markus Koller)
- Add the option to automatically run a pipeline after updating AutoDevOps settings. !15380
- Add total_time_spent to the `changes` hash in issuable Webhook payloads. !15381
- Monitor NFS shards for circuitbreaker in a separate process. !15426
- Add inline editing to issues on mobile. !15438
- Add custom brand text on new project pages. !15541 (Markus Koller)
- Show only group name by default and put full namespace in tooltip in Groups tree. !15650
- Use custom user agent header in all GCP API requests. !15705
- Changed the deploy markers on the prometheus dashboard to be more verbose. !38032
- Animate contextual sidebar on collapse/expand.
- Update emojis. Add :gay_pride_flag: and :speech_left:. Remove extraneous comma in :cartwheel_tone4:.
- When a custom header logo is present, don't show GitLab type logo.
- Improved diff changed files dropdown design.
### Performance (19 changes)
- Add timeouts for Gitaly calls. !15047
- Performance issues when loading large number of wiki pages. !15276
- Add performance logging to UpdateMergeRequestsWorker. !15360
- Keep track of all circuitbreaker keys in a set. !15613
- Improve the performance for counting commits. !15628
- Reduce requests for project forks on show page of projects that have forks. !15663
- Perform SQL matching of Build&Runner tags to greatly speed-up job picking.
- Only load branch names for protected branch checks.
- Optimize API /groups/:id/projects by preloading associations.
- Remove allocation tracking code from InfluxDB sampler for performance.
- Throttle the number of UPDATEs triggered by touch.
- Make finding most recent merge request diffs more efficient.
- Fetch blobs in bulk when generating diffs.
- Cache commits for MergeRequest diffs.
- Use fuzzy search with minimum length of 3 characters where appropriate.
- Add axios to common file.
- Remove template selector from global namespace.
- check the import_status field before doing SQL operations to check the import url.
- Stop sending milestone and labels data over the wire for MR widget requests.
### Added (22 changes, 15 of them are from the community)
- Limit autocomplete menu to applied labels. !11110 (Vitaliy @blackst0ne Klachkov)
- Make diff notes created on a commit in a merge request to persist a rebase. !12148
- Allow creation of merge request from email. !13817 (janp)
- Add an ability to use a custom branch name on creation from issues. !13884 (Vitaliy @blackst0ne Klachkov)
- Add anonymous rate limit per IP, and authenticated (web or API) rate limits per user. !14708
- Create a new form to add Existing Kubernetes Cluster. !14805
- Add support of Mermaid (generation of diagrams and flowcharts from text). !15107 (Vitaliy @blackst0ne Klachkov)
- Add total time spent to milestones. !15116 (George Andrinopoulos)
- Add /groups/:id/subgroups endpoint to API. !15142 (marbemac)
- Add administrative endpoint to list all pages domains. !15160 (Travis Miller)
- Adds Rubocop rule for line break after guard clause. !15188 (Jacopo Beschi @jacopo-beschi)
- Add edit button to mobile file view. !15199 (Travis Miller)
- Add dropdown sort to group milestones. !15230 (George Andrinopoulos)
- added support for ordering and sorting in notes api. !15342 (haseebeqx)
- Hashed Storage migration script now supports migrating project attachments. !15352
- New API endpoint - list jobs for a specified runner. !15432
- Add new API endpoint - get a namespace by ID. !15442
- Disables autocomplete in filtered searc. !15477 (Jacopo Beschi @jacopo-beschi)
- Update empty state page of merge request 'changes' tab. !15611 (Vitaliy @blackst0ne Klachkov)
- Allow git pull/push on group/user/project redirects. !15670
- show status of gitlab reference links in wiki. !15694 (haseebeqx)
- Add email confirmation parameters for user creation and update via API. (Daniel Juarez)
### Other (17 changes, 7 of them are from the community)
- Enable UnnecessaryMantissa in scss-lint. !15255 (Takuya Noguchi)
- Add untracked files to uploads table. !15270
- Move update_project_counter_caches? out of issue and merge request. !15300 (George Andrinopoulos)
- Removed tooltip from clone dropdown. !15334
- Clean up empty fork networks. !15373
- Create issuable destroy service. !15604 (George Andrinopoulos)
- Upgrade seed-fu to 2.3.7. !15607 (Takuya Noguchi)
- Rename GKE as Kubernetes Engine. !15608 (Takuya Noguchi)
- Prefer ci_config_path validation for leading slashes instead of sanitizing the input. !15672 (Christiaan Van den Poel)
- Fix typo in docs about Elasticsearch. !15699 (Takuya Noguchi)
- Add internationalization support for the prometheus integration. !33338
- Export text utils functions as es6 module and add tests.
- Stop reloading the page when using pagination and tabs - use API calls - in Pipelines table.
- Clean up schema of the "issues" table.
- Clarify wording of protected branch settings for the default branch.
- Update svg external depencency.
- Clean up schema of the "merge_requests" table.
## 10.2.5 (2017-12-15)
### Fixed (8 changes)

View file

@ -1 +1 @@
0.60.0
0.64.0

14
Gemfile
View file

@ -12,7 +12,7 @@ gem 'sprockets', '~> 3.7.0'
gem 'default_value_for', '~> 3.0.0'
# Supported DBs
gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.26.0'
@ -283,7 +283,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta43'
gem 'prometheus-client-mmap', '~> 0.7.0.beta44'
gem 'raindrops', '~> 0.18'
end
@ -334,9 +334,11 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'rubocop', '~> 0.49.1', require: false
gem 'rubocop-rspec', '~> 1.15.1', require: false
gem 'rubocop-gitlab-security', '~> 0.1.0', require: false
gem 'gitlab-styles', '~> 2.2.0', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
gem 'rubocop', '~> 0.52.0'
gem 'rubocop-rspec', '~> 1.20.1'
gem 'scss_lint', '~> 0.54.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
@ -400,7 +402,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.61.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.62.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false

View file

@ -284,7 +284,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.61.0)
gitaly-proto (0.62.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@ -303,6 +303,10 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.3)
gitlab-styles (2.2.0)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
@ -501,7 +505,7 @@ GEM
mustermann (1.0.0)
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
mysql2 (0.4.5)
mysql2 (0.4.10)
net-ldap (0.16.0)
net-ssh (4.1.0)
netrc (0.11.0)
@ -630,7 +634,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta43)
prometheus-client-mmap (0.7.0.beta44)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@ -704,7 +708,7 @@ GEM
json
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
redis (3.3.3)
redis (3.3.5)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
@ -777,21 +781,21 @@ GEM
pg
rails
sqlite3
rubocop (0.49.1)
rubocop (0.52.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
parser (>= 2.4.0.2, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.0)
rubocop (>= 0.47.1)
rubocop-rspec (1.15.1)
rubocop (>= 0.42.0)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
rubocop-rspec (1.20.1)
rubocop (>= 0.51.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.16.2)
ruby-progressbar (1.8.1)
ruby-progressbar (1.9.0)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.9.0)
@ -835,11 +839,11 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
sidekiq (5.0.4)
sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.3, >= 3.3.3)
redis (>= 3.3.4, < 5)
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
@ -1042,10 +1046,11 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.61.0)
gitaly-proto (~> 0.62.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.2.0)
gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
@ -1082,7 +1087,7 @@ DEPENDENCIES
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.5)
mysql2 (~> 0.4.10)
net-ldap
net-ssh (~> 4.1.0)
nokogiri (~> 1.8.1)
@ -1117,7 +1122,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta43)
prometheus-client-mmap (~> 0.7.0.beta44)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@ -1148,9 +1153,8 @@ DEPENDENCIES
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.49.1)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.15.1)
rubocop (~> 0.52.0)
rubocop-rspec (~> 1.20.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8)

View file

@ -1 +1 @@
10.3.0-pre
10.4.0-pre

View file

@ -1 +1 @@
{"iconCount":181,"spriteSize":81482,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
{"iconCount":186,"spriteSize":84748,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><rect width="7" height="1" x="59" y="38" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M60.5 42a3.5 3.5 0 0 0 0-7v7z"/><rect width="7" height="1" x="12" y="38" fill="#E1DBF2" transform="matrix(-1 0 0 1 31 0)" rx=".5"/><path fill="#6B4FBB" d="M17.5 42a3.5 3.5 0 0 1 0-7v7z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M39 58c10.493 0 19-8.507 19-19s-8.507-19-19-19-19 8.507-19 19 8.507 19 19 19zm0 4c-12.703 0-23-10.297-23-23s10.297-23 23-23 23 10.297 23 23-10.297 23-23 23z"/><path fill="#6B4FBB" d="M35 56a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M26.5 40c0 4.143 3.355 7.5 7.494 7.5h10.012A7.497 7.497 0 0 0 51.5 40c0-4.143-3.355-7.5-7.494-7.5H33.994A7.497 7.497 0 0 0 26.5 40zm-3 0c0-5.799 4.698-10.5 10.494-10.5h10.012C49.802 29.5 54.5 34.2 54.5 40c0 5.799-4.698 10.5-10.494 10.5H33.994C28.198 50.5 23.5 45.8 23.5 40z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M35.255 42.406a1 1 0 1 1 1.872-.703 2.001 2.001 0 0 0 3.76-.038 1 1 0 1 1 1.886.665 4 4 0 0 1-7.518.076zM31.5 40a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm15 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/><path fill="#6B4FBB" d="M38 22h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zm0 3h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z" style="mix-blend-mode:multiply"/></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -1,4 +1,5 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
const Api = {
groupsPath: '/api/:version/groups.json',
@ -6,6 +7,7 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key',
@ -76,6 +78,14 @@ const Api = {
.done(projects => callback(projects));
},
// Return single project
project(projectPath) {
const url = Api.buildUrl(Api.projectPath)
.replace(':id', encodeURIComponent(projectPath));
return axios.get(url);
},
newLabel(namespacePath, projectPath, data, callback) {
let url;
@ -115,7 +125,7 @@ const Api = {
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
.replace(':id', id);
.replace(':id', encodeURIComponent(id));
return this.wrapAjaxCall({
url,
type: 'POST',
@ -127,7 +137,7 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', id)
.replace(':id', encodeURIComponent(id))
.replace(':branch', branch);
return this.wrapAjaxCall({

View file

@ -102,7 +102,6 @@ $(() => {
if (list.type === 'closed') {
list.position = Infinity;
list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} else if (list.type === 'backlog') {
list.position = -1;
}

View file

@ -1,5 +1,4 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global MilestoneSelect */
import Vue from 'vue';
import Flash from '../../flash';
@ -12,6 +11,7 @@ import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore;

View file

@ -30,6 +30,7 @@ export default class Clusters {
installHelmPath,
installIngressPath,
installRunnerPath,
installPrometheusPath,
clusterStatus,
clusterStatusReason,
helpPath,
@ -44,6 +45,7 @@ export default class Clusters {
installHelmEndpoint: installHelmPath,
installIngressEndpoint: installIngressPath,
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
});
this.toggle = this.toggle.bind(this);

View file

@ -67,6 +67,16 @@ export default {
and send the results back to GitLab.`,
));
},
prometheusDescription() {
return sprintf(
_.escape(s__('ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications.')), {
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html", target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|Gitlab Integration'))}
</a>`,
},
false,
);
},
},
};
</script>
@ -105,6 +115,16 @@ export default {
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
/>
<application-row
id="prometheus"
:title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/"
:description="prometheusDescription"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
/>
<!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests -->
<!-- Add GitLab Runner row, all other plumbing is complete -->

View file

@ -7,6 +7,7 @@ export default class ClusterService {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
};
}

View file

@ -28,6 +28,13 @@ export default class ClusterStore {
requestStatus: null,
requestReason: null,
},
prometheus: {
title: s__('ClusterIntegration|Prometheus'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
},
},
};
}

View file

@ -2,6 +2,7 @@
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
@ -12,6 +13,7 @@
userAvatarImage,
totalTime,
limitWarning,
icon,
},
};
</script>
@ -52,7 +54,10 @@
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<icon
name="fork"
:size="16">
</icon>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>

View file

@ -3,6 +3,7 @@
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
@ -13,6 +14,7 @@
userAvatarImage,
totalTime,
limitWarning,
icon,
},
computed: {
iconBranch() {
@ -37,7 +39,10 @@
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<icon
name="fork"
:size="16">
</icon>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>

View file

@ -3,6 +3,7 @@
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
@ -12,6 +13,7 @@
components: {
totalTime,
limitWarning,
icon,
},
computed: {
iconBuildStatus() {
@ -40,7 +42,10 @@
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<icon
name="fork"
:size="16">
</icon>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>

View file

@ -5,7 +5,7 @@ import IssuableIndex from './issuable_index';
import Milestone from './milestone';
import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
import MilestoneSelect from './milestone_select';
import NewBranchForm from './new_branch_form';
import NotificationsForm from './notifications_form';
import notificationsDropdown from './notifications_dropdown';
@ -73,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports';
@ -111,6 +110,8 @@ import Activities from './activities';
return false;
}
const fail = () => Flash('Error loading dynamic module');
path = page.split(':');
shortcut_handler = null;
@ -447,9 +448,6 @@ import Activities from './activities';
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
if (UserFeatureHelper.isNewRepoEnabled()) break;
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
@ -468,7 +466,6 @@ import Activities from './activities';
shortcut_handler = true;
break;
case 'projects:blob:show':
if (UserFeatureHelper.isNewRepoEnabled()) break;
new BlobViewer();
initBlob();
break;
@ -545,7 +542,7 @@ import Activities from './activities';
new CILintEditor();
break;
case 'users:show':
new UserCallout();
import('./pages/users/show').then(m => m.default()).catch(fail);
break;
case 'admin:conversational_development_index:show':
new UserCallout();

View file

@ -127,7 +127,7 @@ class FilteredSearchManager {
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.call(this);
this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
@ -180,22 +180,34 @@ class FilteredSearchManager {
this.unbindStateEvents();
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
checkForBackspace() {
let backspaceCount = 0;
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
// closure for keeping track of the number of backspace keystrokes
return (e) => {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
backspaceCount += 1;
if (backspaceCount === 2) {
backspaceCount = 0;
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
}
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
} else {
backspaceCount = 0;
}
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
}
};
}
checkForEnter(e) {

View file

@ -161,13 +161,16 @@ export default () => {
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
const topItems = sidebar.querySelector('.sidebar-top-level-items');
if (topItems) {
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (currentOpenMenu) hideMenu(currentOpenMenu);
}, getHideSubItemsInterval());
});
timeoutId = setTimeout(() => {
if (currentOpenMenu) hideMenu(currentOpenMenu);
}, getHideSubItemsInterval());
});
}
headerHeight = document.querySelector('.nav-sidebar').offsetTop;

View file

@ -1,7 +0,0 @@
import Cookies from 'js-cookie';
export default {
isNewRepoEnabled() {
return Cookies.get('new_repo') === 'true';
},
};

View file

@ -0,0 +1,66 @@
<script>
import { mapState } from 'vuex';
import icon from '../../../vue_shared/components/icon.vue';
import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue';
export default {
components: {
icon,
listItem,
listCollapsed,
},
props: {
title: {
type: String,
required: true,
},
fileList: {
type: Array,
required: true,
},
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed');
},
},
};
</script>
<template>
<div class="multi-file-commit-list">
<list-collapsed
v-if="rightPanelCollapsed"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
/>
</li>
</ul>
<div
v-else
class="help-block prepend-top-0"
>
No changes
</div>
</template>
</div>
</template>

View file

@ -0,0 +1,73 @@
<script>
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue';
export default {
computed: {
...mapState([
'currentBlobView',
'selectedFile',
]),
...mapGetters([
'changedFiles',
'activeFile',
]),
},
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
repoPreview,
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
},
};
</script>
<template>
<div
class="ide-view"
>
<ide-sidebar/>
<div
class="multi-file-edit-pane"
>
<template
v-if="activeFile">
<repo-tabs/>
<component
class="multi-file-edit-pane-content"
:is="currentBlobView"
/>
<repo-file-buttons/>
<ide-status-bar
:file="selectedFile"/>
</template>
<template
v-else>
<div class="ide-empty-state">
<h2 class="clgray">Welcome to the GitLab IDE</h2>
</div>
</template>
</div>
<ide-contextbar/>
</div>
</template>

View file

@ -0,0 +1,75 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import repoCommitSection from './repo_commit_section.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
repoCommitSection,
icon,
},
computed: {
...mapState([
'rightPanelCollapsed',
]),
...mapGetters([
'changedFiles',
]),
currentIcon() {
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-section">
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed">
<icon
name="list-bulleted"
:size="18"
/>
Staged
</div>
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click="toggleCollapsed"
>
<icon
:name="currentIcon"
:size="18"
/>
</button>
</header>
<repo-commit-section
class=""/>
</div>
</div>
</template>

View file

@ -0,0 +1,47 @@
<script>
import repoTree from './ide_repo_tree.vue';
import icon from '../../vue_shared/components/icon.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
repoTree,
icon,
newDropdown,
},
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="branch-container">
<div class="branch-header">
<div class="branch-header-title">
<icon
name="branch"
:size="12">
</icon>
{{ branch.name }}
</div>
<div class="branch-header-btns">
<new-dropdown
:project-id="projectId"
:branch="branch.name"
path=""/>
</div>
</div>
<div>
<repo-tree
:treeId="branch.treeId"/>
</div>
</div>
</template>

View file

@ -0,0 +1,47 @@
<script>
import branchesTree from './ide_project_branches_tree.vue';
import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
export default {
components: {
branchesTree,
projectAvatarImage,
},
props: {
project: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="projects-sidebar">
<div class="context-header">
<a
:title="project.name"
:href="project.web_url">
<div class="avatar-container s40 project-avatar">
<project-avatar-image
class="avatar-container project-avatar"
:link-href="project.path"
:img-src="project.avatar_url"
:img-alt="project.name"
:img-size="40"
/>
</div>
<div class="sidebar-context-title">
{{ project.name }}
</div>
</a>
</div>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="(branch, index) in project.branches"
:key="branch.name"
:project-id="project.path_with_namespace"
:branch="branch"/>
</div>
</div>
</template>

View file

@ -0,0 +1,66 @@
<script>
import { mapState } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
import { treeList } from '../stores/utils';
export default {
components: {
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
props: {
treeId: {
type: String,
required: true,
},
},
computed: {
...mapState([
'loading',
'isRoot',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
fetchedList() {
return treeList(this.$store.state, this.treeId);
},
hasPreviousDirectory() {
return !this.isRoot && this.fetchedList.length;
},
showLoading() {
return this.loading;
},
},
};
</script>
<template>
<div>
<div class="ide-file-list">
<table class="table">
<tbody
v-if="treeId">
<repo-previous-directory
v-if="hasPreviousDirectory"
/>
<repo-loading-file
v-if="showLoading"
v-for="n in 5"
:key="n"
/>
<repo-file
v-for="file in fetchedList"
:key="file.key"
:file="file"
/>
</tbody>
</table>
</div>
</div>
</template>

View file

@ -0,0 +1,62 @@
<script>
import { mapState, mapActions } from 'vuex';
import projectTree from './ide_project_tree.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
projectTree,
icon,
},
computed: {
...mapState([
'projects',
'leftPanelCollapsed',
]),
currentIcon() {
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'left',
collapsed: !this.leftPanelCollapsed,
});
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': leftPanelCollapsed,
}"
>
<div class="multi-file-commit-panel-inner">
<project-tree
v-for="(project, index) in projects"
:key="project.id"
:project="project"/>
</div>
<button
type="button"
class="btn btn-transparent left-collapse-btn"
@click="toggleCollapsed"
>
<icon
:name="currentIcon"
:size="18"
/>
<span
v-if="!leftPanelCollapsed"
class="collapse-text"
>Collapse sidebar</span>
</button>
</div>
</template>

View file

@ -0,0 +1,71 @@
<script>
import { mapState } from 'vuex';
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
export default {
props: {
file: {
type: Object,
required: true,
},
},
components: {
icon,
},
directives: {
tooltip,
},
mixins: [
timeAgoMixin,
],
computed: {
...mapState([
'selectedFile',
]),
},
};
</script>
<template>
<div
class="ide-status-bar">
<div>
<icon
name="branch"
:size="12">
</icon>
{{ selectedFile.branchId }}
</div>
<div>
<div
v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
Last commit:
<a
v-tooltip
:title="selectedFile.lastCommit.message"
:href="selectedFile.lastCommit.url">
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
{{ selectedFile.lastCommit.author }}
</a>
</div>
</div>
<div
class="text-right">
{{ selectedFile.name }}
</div>
<div
class="text-right">
{{ selectedFile.eol }}
</div>
<div
class="text-right">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div
class="text-right">
{{ selectedFile.fileLanguage }}
</div>
</div>
</template>

View file

@ -44,7 +44,7 @@
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = this.currentBranch;
this.dropdownText.textContent = this.currentBranchId;
}
this.toggleDropdown();

View file

@ -0,0 +1,101 @@
<script>
import newModal from './modal.vue';
import upload from './upload.vue';
import icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
branch: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
components: {
icon,
newModal,
upload,
},
data() {
return {
openModal: false,
modalType: '',
};
},
methods: {
createNewItem(type) {
this.modalType = type;
this.toggleModalOpen();
},
toggleModalOpen() {
this.openModal = !this.openModal;
},
},
};
</script>
<template>
<div class="repo-new-btn pull-right">
<div class="dropdown">
<button
type="button"
class="btn btn-sm btn-default dropdown-toggle add-to-tree"
data-toggle="dropdown"
aria-label="Create new file or directory"
>
<icon
name="plus"
:size="12"
css-classes="pull-left"
/>
<icon
name="arrow-down"
:size="12"
css-classes="pull-left"
/>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<upload
:branch-id="branch"
:path="path"
:parent="parent"
/>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</div>
<new-modal
v-if="openModal"
:type="modalType"
:branch-id="branch"
:path="path"
:parent="parent"
@toggle="toggleModalOpen"
/>
</div>
</template>

View file

@ -1,10 +1,18 @@
<script>
import { mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale';
import modal from '../../../vue_shared/components/modal.vue';
export default {
props: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
type: {
type: String,
required: true,
@ -28,6 +36,9 @@
]),
createEntryInStore() {
this.createTempEntry({
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
@ -39,6 +50,9 @@
},
},
computed: {
...mapState([
'currentProjectId',
]),
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');

View file

@ -1,12 +1,22 @@
<script>
import { mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
export default {
props: {
path: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
computed: {
...mapState([
'trees',
'currentProjectId',
]),
},
methods: {
...mapActions([
@ -22,6 +32,9 @@
this.createTempEntry({
name,
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
type: 'blob',
content: result,
base64: !isText,
@ -42,6 +55,9 @@
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
startFileUpload() {
this.$refs.fileUpload.click();
},
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
@ -53,16 +69,19 @@
</script>
<template>
<label
role="button"
class="menu-item"
>
{{ __('Upload file') }}
<div>
<a
href="#"
role="button"
@click.prevent="startFileUpload"
>
{{ __('Upload file') }}
</a>
<input
id="file-upload"
type="file"
class="hidden"
ref="fileUpload"
/>
</label>
</div>
</template>

View file

@ -20,12 +20,13 @@ export default {
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
collapsed: true,
};
},
computed: {
...mapState([
'currentBranch',
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
...mapGetters([
'changedFiles',
@ -42,12 +43,13 @@ export default {
'checkCommitStatus',
'commitChanges',
'getTreeData',
'setPanelCollapsedStatus',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
const payload = {
branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
@ -55,7 +57,7 @@ export default {
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
start_branch: createNewBranch ? this.currentBranch : undefined,
start_branch: createNewBranch ? this.currentBranchId : undefined,
};
this.showNewBranchModal = false;
@ -64,7 +66,12 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
this.getTreeData();
this.$store.dispatch('getTreeData', {
projectId: this.currentProjectId,
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
})
.catch(() => {
this.submitCommitsLoading = false;
@ -86,19 +93,17 @@ export default {
});
},
toggleCollapsed() {
this.collapsed = !this.collapsed;
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed,
}"
>
<div class="multi-file-commit-panel-section">
<modal
v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')"
@ -108,28 +113,16 @@ export default {
@toggle="showNewBranchModal = false"
@submit="makeCommit(true)"
/>
<button
v-if="collapsed"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
@click="toggleCollapsed"
>
<i
aria-hidden="true"
class="fa fa-angle-double-left"
>
</i>
</button>
<commit-files-list
title="Staged"
:file-list="changedFiles"
:collapsed="collapsed"
:collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit"
v-if="!collapsed"
v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea

View file

@ -1,6 +1,6 @@
<script>
/* global monaco */
import { mapGetters, mapActions } from 'vuex';
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
@ -24,6 +24,9 @@ export default {
...mapActions([
'getRawFileData',
'changeFileContent',
'setFileLanguage',
'setEditorPosition',
'setFileEOL',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@ -43,12 +46,36 @@ export default {
const model = this.editor.createModel(this.activeFile);
this.editor.attachModel(model);
model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
content: m.getValue(),
});
});
// Handle Cursor Position
this.editor.onPositionChange((instance, e) => {
this.setEditorPosition({
editorRow: e.position.lineNumber,
editorColumn: e.position.column,
});
});
this.editor.setPosition({
lineNumber: this.activeFile.editorRow,
column: this.activeFile.editorColumn,
});
// Handle File Language
this.setFileLanguage({
fileLanguage: model.language,
});
// Get File eol
this.setFileEOL({
eol: model.eol,
});
},
},
watch: {
@ -57,12 +84,22 @@ export default {
this.initMonaco();
}
},
leftPanelCollapsed() {
this.editor.updateDimensions();
},
rightPanelCollapsed() {
this.editor.updateDimensions();
},
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
]),
shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw;
},
@ -76,13 +113,14 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
v-show="shouldHideEditor"
v-if="shouldHideEditor"
v-html="activeFile.html"
>
</div>
<div
v-show="!shouldHideEditor"
ref="editor"
class="multi-file-editor-holder"
>
</div>
</div>

View file

@ -1,7 +1,8 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
mixins: [
@ -9,20 +10,22 @@
],
components: {
skeletonLoadingContainer,
newDropdown,
},
props: {
file: {
type: Object,
required: true,
},
showExtraColumns: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters([
'isCollapsed',
...mapState([
'leftPanelCollapsed',
]),
isSubmodule() {
return this.file.type === 'submodule';
},
fileIcon() {
return {
'fa-spinner fa-spin': this.file.loading,
@ -30,6 +33,12 @@
'fa-folder-open': !this.file.loading && this.file.opened,
};
},
isSubmodule() {
return this.file.type === 'submodule';
},
isTree() {
return this.file.type === 'tree';
},
levelIndentation() {
return {
marginLeft: `${this.file.level * 16}px`,
@ -39,13 +48,39 @@
return this.file.id.substr(0, 8);
},
submoduleColSpan() {
return !this.isCollapsed && this.isSubmodule ? 3 : 1;
return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
},
fileClass() {
if (this.file.type === 'blob') {
if (this.file.active) {
return 'file-open file-active';
}
return this.file.opened ? 'file-open' : '';
}
return '';
},
changedClass() {
return {
'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
};
},
},
methods: {
...mapActions([
'clickedTreeRow',
]),
clickFile(row) {
// Manual Action if a tree is selected/opened
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
this.$store.dispatch('toggleTreeOpen', {
endpoint: this.file.url,
tree: this.file,
});
}
this.$router.push(`/project${row.url}`);
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
},
};
</script>
@ -53,7 +88,8 @@
<template>
<tr
class="file"
@click.prevent="clickedTreeRow(file)">
:class="fileClass"
@click="clickFile(file)">
<td
class="multi-file-table-name"
:colspan="submoduleColSpan"
@ -66,11 +102,23 @@
>
</i>
<a
:href="file.url"
class="repo-file-name"
>
{{ file.name }}
</a>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
:parent="file"/>
<i
class="fa"
v-if="changedClass"
:class="changedClass"
aria-hidden="true"
>
</i>
<template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
@ -84,7 +132,7 @@
</template>
</td>
<template v-if="!isCollapsed && !isSubmodule">
<template v-if="showExtraColumns && !isSubmodule">
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a
v-if="file.lastCommit.message"

View file

@ -1,5 +1,5 @@
<script>
import { mapGetters } from 'vuex';
import { mapState } from 'vuex';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
@ -7,8 +7,8 @@
skeletonLoadingContainer,
},
computed: {
...mapGetters([
'isCollapsed',
...mapState([
'leftPanelCollapsed',
]),
},
};
@ -24,7 +24,7 @@
:small="true"
/>
</td>
<template v-if="!isCollapsed">
<template v-if="!leftPanelCollapsed">
<td
class="hidden-sm hidden-xs">
<skeleton-loading-container

View file

@ -1,16 +1,14 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState([
'parentTreeUrl',
]),
...mapGetters([
'isCollapsed',
'leftPanelCollapsed',
]),
colSpanCondition() {
return this.isCollapsed ? undefined : 3;
return this.leftPanelCollapsed ? undefined : 3;
},
},
methods: {

View file

@ -27,16 +27,18 @@ export default {
methods: {
...mapActions([
'setFileActive',
'closeFile',
]),
clickFile(tab) {
this.$router.push(`/project${tab.url}`);
},
},
};
</script>
<template>
<li
@click="setFileActive(tab)"
@click="clickFile(tab)"
>
<button
type="button"

View file

@ -0,0 +1,101 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from './stores';
import flash from '../flash';
import {
getTreeEntry,
} from './stores/utils';
Vue.use(VueRouter);
/**
* Routes below /-/ide/:
/project/h5bp/html5-boilerplate/blob/master
/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
/project/h5bp/html5-boilerplate/mr/123
/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
/workspace/123
/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
/workspace/project/h5bp/html5-boilerplate/mr/123
/ = /workspace
/settings
*/
// Unfortunately Vue Router doesn't work without at least a fake component
// If you do only data handling
const EmptyRouterComponent = {
render(createElement) {
return createElement('div');
},
};
const router = new VueRouter({
mode: 'history',
base: `${gon.relative_url_root}/-/ide/`,
routes: [
{
path: '/project/:namespace/:project',
component: EmptyRouterComponent,
children: [
{
path: ':targetmode/:branch/*',
component: EmptyRouterComponent,
},
{
path: 'mr/:mrid',
component: EmptyRouterComponent,
},
],
},
],
});
router.beforeEach((to, from, next) => {
if (to.params.namespace && to.params.project) {
store.dispatch('getProjectData', {
namespace: to.params.namespace,
projectId: to.params.project,
})
.then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) {
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: to.params.branch,
});
store.dispatch('getTreeData', {
projectId: fullProjectId,
branch: to.params.branch,
endpoint: `/tree/${to.params.branch}`,
})
.then(() => {
if (to.params[0]) {
const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
}
})
.catch((e) => {
flash('Error while loading the branch files. Please try again.');
throw e;
});
}
})
.catch((e) => {
flash('Error while loading the project data. Please try again.');
throw e;
});
}
next();
});
export default router;

View file

@ -0,0 +1,55 @@
import Vue from 'vue';
import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
import Translate from '../vue_shared/translate';
import ContextualSidebar from '../contextual_sidebar';
function initIde(el) {
if (!el) return null;
return new Vue({
el,
store,
router,
components: {
ide,
},
methods: {
...mapActions([
'setInitialData',
]),
},
created() {
const data = el.dataset;
this.setInitialData({
endpoints: {
rootEndpoint: data.url,
newMergeRequestUrl: data.newMergeRequestUrl,
rootUrl: data.rootUrl,
},
canCommit: convertPermissionToBoolean(data.canCommit),
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
path: data.currentPath,
isRoot: convertPermissionToBoolean(data.root),
isInitialRoot: convertPermissionToBoolean(data.root),
});
},
render(createElement) {
return createElement('ide');
},
});
}
const ideElement = document.getElementById('ide');
Vue.use(Translate);
initIde(ideElement);
const contextualSidebar = new ContextualSidebar();
contextualSidebar.bindEvents();

View file

@ -28,6 +28,14 @@ export default class Model {
return this.model.uri.toString();
}
get language() {
return this.model.getModeId();
}
get eol() {
return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
}
get path() {
return this.file.path;
}

View file

@ -22,6 +22,11 @@ export default class Editor {
this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this),
);
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
}, 200);
window.addEventListener('resize', this.debouncedUpdate, false);
}
createInstance(domElement) {
@ -32,6 +37,9 @@ export default class Editor {
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
@ -70,10 +78,32 @@ export default class Editor {
dispose() {
this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance
if (this.instance) {
this.instance = null;
}
}
updateDimensions() {
this.instance.layout();
}
setPosition({ lineNumber, column }) {
this.instance.revealPositionInCenter({
lineNumber,
column,
});
this.instance.setPosition({
lineNumber,
column,
});
}
onPositionChange(cb) {
this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
}
}

View file

@ -23,8 +23,11 @@ export default {
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text());
},
getBranchData(projectId, currentBranch) {
return Api.branchSingle(projectId, currentBranch);
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);

View file

@ -0,0 +1,179 @@
import Vue from 'vue';
import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const closeDiscardPopup = ({ commit }) =>
commit(types.TOGGLE_DISCARD_POPUP, false);
export const discardAllChanges = ({ commit, getters, dispatch }) => {
const changedFiles = getters.changedFiles;
changedFiles.forEach((file) => {
commit(types.DISCARD_FILE_CHANGES, file);
if (file.tempFile) {
dispatch('closeFile', { file, force: true });
}
});
};
export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', { file }));
};
export const toggleEditMode = (
{ state, commit, getters, dispatch },
force = false,
) => {
const changedFiles = getters.changedFiles;
if (changedFiles.length && !force) {
commit(types.TOGGLE_DISCARD_POPUP, true);
} else {
commit(types.TOGGLE_EDIT_MODE);
commit(types.TOGGLE_DISCARD_POPUP, false);
dispatch('toggleBlobView');
if (!state.editMode) {
dispatch('discardAllChanges');
}
}
};
export const toggleBlobView = ({ commit, state }) => {
if (state.editMode) {
commit(types.SET_EDIT_MODE);
} else {
commit(types.SET_PREVIEW_MODE);
}
};
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
if (side === 'left') {
commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
} else {
commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
}
};
export const checkCommitStatus = ({ state }) =>
service
.getBranchData(state.currentProjectId, state.currentBranchId)
.then((data) => {
const { id } = data.commit;
const selectedBranch =
state.projects[state.currentProjectId].branches[state.currentBranchId];
if (selectedBranch.workingReference !== id) {
return true;
}
return false;
})
.catch(() => flash('Error checking branch data. Please try again.'));
export const commitChanges = (
{ commit, state, dispatch, getters },
{ payload, newMr },
) =>
service
.commit(state.currentProjectId, payload)
.then((data) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message);
return;
}
const selectedProject = state.projects[state.currentProjectId];
const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
commit: {
message: data.message,
authored_date: data.committed_date,
},
};
flash(
`Your changes have been committed. Commit ${data.short_id} with ${
data.stats.additions
} additions, ${data.stats.deletions} deletions.`,
'notice',
);
if (newMr) {
dispatch(
'redirectToUrl',
`${
selectedProject.web_url
}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
);
} else {
commit(types.SET_BRANCH_WORKING_REFERENCE, {
projectId: state.currentProjectId,
branchId: state.currentBranchId,
reference: data.id,
});
getters.changedFiles.forEach((entry) => {
commit(types.SET_LAST_COMMIT_DATA, {
entry,
lastCommit,
});
});
dispatch('discardAllChanges');
dispatch('closeAllFiles');
window.scrollTo(0, 0);
}
})
.catch(() => flash('Error committing changes. Please try again.'));
export const createTempEntry = (
{ state, dispatch },
{ projectId, branchId, parent, name, type, content = '', base64 = false },
) => {
const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
if (type === 'tree') {
dispatch('createTempTree', {
projectId,
branchId,
parent: selectedParent,
name,
});
} else if (type === 'blob') {
dispatch('createTempFile', {
projectId,
branchId,
parent: selectedParent,
name,
base64,
content,
});
}
};
export const scrollToTab = () => {
Vue.nextTick(() => {
const tabs = document.getElementById('tabs');
if (tabs) {
const tabEl = tabs.querySelector('.active .repo-tab');
tabEl.focus();
}
});
};
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/branch';

View file

@ -0,0 +1,43 @@
import service from '../../services';
import flash from '../../../flash';
import * as types from '../mutation_types';
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => {
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId])
|| force) {
service.getBranchData(`${projectId}`, branchId)
.then((data) => {
const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.');
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
state.currentProjectId,
{
branch,
ref: state.currentBranchId,
},
)
.then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(state.currentBranchId, branchName);
if (this.$router) this.$router.push(url);
commit(types.SET_CURRENT_BRANCH, branchName);
});

View file

@ -2,9 +2,9 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
findEntry,
pushState,
setPageTitle,
createTemp,
findIndexOfFile,
@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
dispatch('setFileActive', nextFileToOpen);
} else if (!state.openFiles.length) {
pushState(file.parentTreeUrl);
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
dispatch('getLastCommitData');
@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
// reset hash for line highlighting
location.hash = '';
commit(types.SET_CURRENT_PROJECT, file.projectId);
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
export const getFileData = ({ state, commit, dispatch }, file) => {
@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file);
pushState(file.url);
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
};
export const setFileEOL = ({ state, commit }, { eol }) => {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
const path = parent.path !== undefined ? parent.path : '';
// We need to do the replacement otherwise the web_url + file.url duplicate
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({
name: name.replace(`${state.path}/`, ''),
path: tree.path,
projectId,
branchId,
name: name.replace(`${path}/`, ''),
path,
type: 'blob',
level: tree.level !== undefined ? tree.level + 1 : 0,
level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true,
content,
base64,
url: newUrl,
});
if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
commit(types.CREATE_TMP_FILE, {
parent: tree,
parent,
file,
});
commit(types.TOGGLE_FILE_OPEN, file);
@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten
dispatch('toggleEditMode', true);
}
router.push(`/project${file.url}`);
return Promise.resolve(file);
};

View file

@ -0,0 +1,25 @@
import service from '../../services';
import flash from '../../../flash';
import * as types from '../mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
service.getProjectData(namespace, projectId)
.then(res => res.data)
.then((data) => {
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash('Error loading project data. Please try again.');
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});

View file

@ -0,0 +1,188 @@
import { visitUrl } from '../../../lib/utils/url_utility';
import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
setPageTitle,
findEntry,
createTemp,
createOrMergeEntry,
} from '../utils';
export const getTreeData = (
{ commit, state, dispatch },
{ endpoint, tree = null, projectId, branch, force = false } = {},
) => new Promise((resolve, reject) => {
// We already have the base tree so we resolve immediately
if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
resolve();
} else {
if (tree) commit(types.TOGGLE_LOADING, tree);
const selectedProject = state.projects[projectId];
// We are merging the web_url that we got on the project info with the endpoint
// we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
if (completeEndpoint && (!tree || !tree.tempFile)) {
service.getTreeData(completeEndpoint)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
dispatch('updateDirectoryData', { data, tree, projectId, branch });
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
if (tree) commit(types.TOGGLE_LOADING, selectedTree);
const prevLastCommitPath = selectedTree.lastCommitPath;
if (prevLastCommitPath !== null) {
dispatch('getLastCommitData', selectedTree);
}
resolve(data);
})
.catch((e) => {
flash('Error loading tree data. Please try again.');
if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e);
});
} else {
resolve();
}
}
});
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
if (tree.opened) {
// send empty data to clear the tree
const data = { trees: [], blobs: [], submodules: [] };
dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
} else {
dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
}
commit(types.TOGGLE_TREE_OPEN, tree);
};
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', {
endpoint: row.url,
tree: row,
});
} else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row);
visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row);
} else {
dispatch('getFileData', row);
}
};
export const createTempTree = (
{ state, commit, dispatch },
{ projectId, branchId, parent, name },
) => {
let selectedTree = parent;
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
dirNames.forEach((dirName) => {
const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
if (!foundEntry) {
const path = selectedTree.path !== undefined ? selectedTree.path : '';
const tmpEntry = createTemp({
projectId,
branchId,
name: dirName,
path,
type: 'tree',
level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
tree: [],
url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
});
commit(types.CREATE_TMP_TREE, {
parent: selectedTree,
tmpEntry,
});
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
router.push(`/project${tmpEntry.url}`);
selectedTree = tmpEntry;
} else {
selectedTree = foundEntry;
}
});
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
.then((data) => {
data.forEach((lastCommit) => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
}
});
dispatch('getLastCommitData', tree);
})
.catch(() => flash('Error fetching log data.'));
};
export const updateDirectoryData = (
{ commit, state },
{ data, tree, projectId, branch },
) => {
if (!tree) {
const existingTree = state.trees[`${projectId}/${branch}`];
if (!existingTree) {
commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
}
}
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
const createEntry = (entry, type) => createOrMergeEntry({
tree: selectedTree,
projectId: `${projectId}`,
branchId: branch,
entry,
level,
type,
parentTreeUrl,
});
const formattedData = [
...data.trees.map(t => createEntry(t, 'tree')),
...data.submodules.map(m => createEntry(m, 'submodule')),
...data.blobs.map(b => createEntry(b, 'blob')),
];
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
};

View file

@ -0,0 +1,19 @@
export const changedFiles = state => state.openFiles.filter(file => file.changed);
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const activeFileExtension = (state) => {
const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : '';
};
export const canEditFile = (state) => {
const currentActiveFile = activeFile(state);
return state.canCommit &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);

View file

@ -1,16 +1,27 @@
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_COMMIT_REF = 'SET_COMMIT_REF';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
@ -18,6 +29,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
@ -28,3 +42,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';

View file

@ -1,4 +1,5 @@
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@ -32,29 +33,32 @@ export default {
discardPopupOpen,
});
},
[types.SET_COMMIT_REF](state, ref) {
Object.assign(state, {
currentRef: ref,
});
},
[types.SET_ROOT](state, isRoot) {
Object.assign(state, {
isRoot,
isInitialRoot: isRoot,
});
},
[types.SET_PREVIOUS_URL](state, previousUrl) {
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
previousUrl,
leftPanelCollapsed: collapsed,
});
},
[types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
rightPanelCollapsed: collapsed,
});
},
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
});
},
...projectMutations,
...fileMutations,
...treeMutations,
...branchMutations,

View file

@ -0,0 +1,28 @@
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_BRANCH](state, currentBranchId) {
Object.assign(state, {
currentBranchId,
});
},
[types.SET_BRANCH](state, { projectPath, branchName, branch }) {
// Add client side properties
Object.assign(branch, {
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
});
Object.assign(state.projects[projectPath], {
branches: {
[branchName]: branch,
},
});
},
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
Object.assign(state.projects[projectId].branches[branchId], {
workingReference: reference,
});
},
};

View file

@ -6,6 +6,10 @@ export default {
Object.assign(file, {
active,
});
Object.assign(state, {
selectedFile: file,
});
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
@ -42,6 +46,22 @@ export default {
changed,
});
},
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(file, {
fileLanguage,
});
},
[types.SET_FILE_EOL](state, { file, eol }) {
Object.assign(file, {
eol,
});
},
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(file, {
editorRow,
editorColumn,
});
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',

View file

@ -0,0 +1,23 @@
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_PROJECT](state, currentProjectId) {
Object.assign(state, {
currentProjectId,
});
},
[types.SET_PROJECT](state, { projectPath, project }) {
// Add client side properties
Object.assign(project, {
tree: [],
branches: {},
active: true,
});
Object.assign(state, {
projects: Object.assign({}, state.projects, {
[projectPath]: project,
}),
});
},
};

View file

@ -6,6 +6,15 @@ export default {
opened: !tree.opened,
});
},
[types.CREATE_TREE](state, { treePath }) {
Object.assign(state, {
trees: Object.assign({}, state.trees, {
[treePath]: {
tree: [],
},
}),
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, {
tree: data,

View file

@ -1,10 +1,10 @@
export default () => ({
canCommit: false,
currentBranch: '',
currentBlobView: 'repo-preview',
currentRef: '',
currentProjectId: '',
currentBranchId: '',
currentBlobView: 'repo-editor',
discardPopupOpen: false,
editMode: false,
editMode: true,
endpoints: {},
isRoot: false,
isInitialRoot: false,
@ -12,13 +12,11 @@ export default () => ({
loading: false,
onTopOfBranch: false,
openFiles: [],
selectedFile: null,
path: '',
project: {
id: 0,
name: '',
url: '',
},
parentTreeUrl: '',
previousUrl: '',
tree: [],
trees: {},
projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: true,
});

View file

@ -2,6 +2,8 @@ export const dataStructure = () => ({
id: '',
key: '',
type: '',
projectId: '',
branchId: '',
name: '',
url: '',
path: '',
@ -15,9 +17,11 @@ export const dataStructure = () => ({
changed: false,
lastCommitPath: '',
lastCommit: {
id: '',
url: '',
message: '',
updatedAt: '',
author: '',
},
tree_url: '',
blamePath: '',
@ -31,11 +35,17 @@ export const dataStructure = () => ({
parentTreeUrl: '',
renderError: false,
base64: false,
editorRow: 1,
editorColumn: 1,
fileLanguage: '',
eol: '',
});
export const decorateData = (entity) => {
const {
id,
projectId,
branchId,
type,
url,
name,
@ -56,6 +66,8 @@ export const decorateData = (entity) => {
return {
...dataStructure(),
id,
projectId,
branchId,
key: `${name}-${type}-${id}`,
type,
name,
@ -75,24 +87,51 @@ export const decorateData = (entity) => {
};
};
export const findEntry = (state, type, name) => state.tree.find(
/*
Takes the multi-dimensional tree and returns a flattened array.
This allows for the table to recursively render the table rows but keeps the data
structure nested to make it easier to add new files/directories.
*/
export const treeList = (state, treeId) => {
const baseTree = state.trees[treeId];
if (baseTree) {
const mapTree = arr => (!arr.tree || !arr.tree.length ?
[] : _.map(arr.tree, a => [a, mapTree(a)]));
return _.chain(baseTree.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
}
return [];
};
export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
export const getTreeEntry = (store, treeId, path) => {
const fileList = treeList(store.state, treeId);
return fileList ? fileList.find(file => file.path === path) : null;
};
export const findEntry = (tree, type, name) => tree.find(
f => f.type === type && f.name === name,
);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
document.title = title;
};
export const pushState = (url) => {
history.pushState({ url }, '', url);
};
export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
export const createTemp = ({
projectId, branchId, name, path, type, level, changed, content, base64, url,
}) => {
const treePath = path ? `${path}/${name}` : name;
return decorateData({
id: new Date().getTime().toString(),
projectId,
branchId,
name,
type,
tempFile: true,
@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
level,
base64,
renderError: base64,
url,
});
};
export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
const found = findEntry(tree, type, entry.name);
export const createOrMergeEntry = ({ tree,
projectId,
branchId,
entry,
type,
parentTreeUrl,
level }) => {
const found = findEntry(tree.tree || tree, type, entry.name);
if (found) {
return Object.assign({}, found, {
@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level })
return decorateData({
...entry,
projectId,
branchId,
type,
parentTreeUrl,
level,

View file

@ -1,5 +1,6 @@
/* eslint-disable no-new */
/* global MilestoneSelect */
import MilestoneSelect from './milestone_select';
import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
import Sidebar from './right_sidebar';

View file

@ -1,9 +1,9 @@
/* eslint-disable no-new */
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
import MilestoneSelect from './milestone_select';
export default () => {
new UsersSelect();

View file

@ -1,8 +1,7 @@
/* eslint-disable class-methods-use-this, no-new */
/* global MilestoneSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';

View file

@ -1,7 +1,7 @@
import _ from 'underscore';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
import { timeFor } from './lib/utils/datetime_utility';
@ -197,7 +197,7 @@ export default class Job {
// we need to show a message warning the user about that.
if (this.logBytes < log.total) {
// size is in bytes, we need to calculate KiB
const size = bytesToKiB(this.logBytes);
const size = numberToHumanSize(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
} else {

View file

@ -1,237 +1,228 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
/* global ListMilestone */
import _ from 'underscore';
import { timeFor } from './lib/utils/datetime_utility';
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els;
if (currentProject != null) {
_this = this;
this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
$els = $(els);
if (!els) {
$els = $('.js-milestone-select');
}
$els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
issueUpdateURL = $dropdown.data('issueUpdate');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming');
showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
defaultNo = $dropdown.data('default-no');
issuableId = $dropdown.data('issuable-id');
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
$block = $selectbox.closest('.block');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = (showAny ? '' : null);
selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: milestonesUrl
}).done(function(data) {
var extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
});
},
renderRow: function(milestone) {
return `
<li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
</li>
`;
},
filterable: true,
search: {
fields: ['title']
},
selectable: true,
toggleLabel: function(selected, el, e) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
} else {
return defaultLabel;
}
},
defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'),
text: function(milestone) {
return _.escape(milestone.title);
},
id: function(milestone) {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
return milestone.id;
}
},
isSelected: function(milestone) {
return milestone.name === selectedMilestone;
},
hidden: function() {
$selectbox.hide();
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
opened: function(e) {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(clickEvent) {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
}
if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.ModalStore.store.filter;
}
if (boardsStore) {
boardsStore[$dropdown.data('field-name')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
}));
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
$dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
})
.catch(() => {
$loading.fadeOut();
});
} else {
selected = $selectbox.find('input[type="hidden"]').val();
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
data: data
}).done(function(data) {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectbox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = _this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
});
}
}
});
});
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
if (currentProject !== null) {
this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
return MilestoneSelect;
})();
}).call(window);
this.init(els, options);
}
init(els, options) {
let $els = $(els);
if (!els) {
$els = $('.js-milestone-select');
}
$els.each((i, dropdown) => {
let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('project-id');
const milestonesUrl = $dropdown.data('milestones');
const issueUpdateURL = $dropdown.data('issueUpdate');
const showNo = $dropdown.data('show-no');
const showAny = $dropdown.data('show-any');
const showMenuAbove = $dropdown.data('showMenuAbove');
const showUpcoming = $dropdown.data('show-upcoming');
const showStarted = $dropdown.data('show-started');
const useId = $dropdown.data('use-id');
const defaultLabel = $dropdown.data('default-label');
const defaultNo = $dropdown.data('default-no');
const issuableId = $dropdown.data('issuable-id');
const abilityName = $dropdown.data('ability-name');
const $selectBox = $dropdown.closest('.selectbox');
const $block = $selectBox.closest('.block');
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = (showAny ? '' : null);
selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: (term, callback) => $.ajax({
url: milestonesUrl
}).done((data) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
<li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
</li>
`,
filterable: true,
search: {
fields: ['title']
},
selectable: true,
toggleLabel: (selected, el, e) => {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
} else {
return defaultLabel;
}
},
defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'),
text: milestone => _.escape(milestone.title),
id: (milestone) => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
return milestone.id;
}
},
isSelected: milestone => milestone.name === selectedMilestone,
hidden: () => {
$selectBox.hide();
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
opened: (e) => {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
let data, boardsStore;
if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index';
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
}
if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.ModalStore.store.filter;
}
if (boardsStore) {
boardsStore[$dropdown.data('field-name')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
}));
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
$dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(() => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
})
.catch(() => {
$loading.fadeOut();
});
} else {
selected = $selectBox.find('input[type="hidden"]').val();
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
data: data
}).done((data) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
});
}
}
});
});
}
}

View file

@ -6,11 +6,12 @@ export default class NewCommitForm {
this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
this.createMergeRequestContainer = form.find(
'.js-create-merge-request-container',
);
this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
renderDestination() {
var different;
different = this.branchName.val() !== this.originalBranch.val();
@ -23,6 +24,6 @@ export default class NewCommitForm {
this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
}
return this.wasDifferent = different;
return (this.wasDifferent = different);
}
}

View file

@ -0,0 +1,3 @@
import UserCallout from '~/user_callout';
export default () => new UserCallout();

View file

@ -1,5 +1,6 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
@ -11,6 +12,9 @@
directives: {
tooltip,
},
components: {
icon,
},
};
</script>
<template>
@ -24,10 +28,9 @@
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts">
<i
class="fa fa-download"
aria-hidden="true">
</i>
<icon
name="download">
</icon>
<i
class="fa fa-caret-down"
aria-hidden="true">

View file

@ -1,89 +0,0 @@
<script>
import icon from '../../../vue_shared/components/icon.vue';
import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue';
export default {
components: {
icon,
listItem,
listCollapsed,
},
props: {
title: {
type: String,
required: true,
},
fileList: {
type: Array,
required: true,
},
collapsed: {
type: Boolean,
required: true,
},
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed');
},
},
};
</script>
<template>
<div class="multi-file-commit-panel-section">
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': collapsed,
}"
>
<icon
name="list-bulleted"
:size="18"
css-classes="append-right-default"
/>
<template v-if="!collapsed">
{{ title }}
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click="toggleCollapsed"
>
<i
aria-hidden="true"
class="fa fa-angle-double-right"
>
</i>
</button>
</template>
</header>
<div class="multi-file-commit-list">
<list-collapsed
v-if="collapsed"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
/>
</li>
</ul>
<div
v-else
class="help-block prepend-top-0"
>
No changes
</div>
</template>
</div>
</div>
</template>

View file

@ -1,89 +0,0 @@
<script>
import { mapState } from 'vuex';
import newModal from './modal.vue';
import upload from './upload.vue';
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
newModal,
upload,
},
data() {
return {
openModal: false,
modalType: '',
};
},
computed: {
...mapState([
'path',
]),
},
methods: {
createNewItem(type) {
this.modalType = type;
this.toggleModalOpen();
},
toggleModalOpen() {
this.openModal = !this.openModal;
},
},
};
</script>
<template>
<div>
<ul class="breadcrumb repo-breadcrumb">
<li class="dropdown">
<button
type="button"
class="btn btn-default dropdown-toggle add-to-tree"
data-toggle="dropdown"
aria-label="Create new file or directory"
>
<icon
name="plus"
css-classes="pull-left"
/>
<icon
name="arrow-down"
css-classes="pull-left"
/>
</button>
<ul class="dropdown-menu">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<upload
:path="path"
/>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</li>
</ul>
<new-modal
v-if="openModal"
:type="modalType"
:path="path"
@toggle="toggleModalOpen"
/>
</div>
</template>

View file

@ -1,63 +0,0 @@
<script>
import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue';
export default {
computed: {
...mapState([
'currentBlobView',
]),
...mapGetters([
'isCollapsed',
'changedFiles',
]),
},
components: {
RepoSidebar,
RepoTabs,
RepoFileButtons,
repoEditor,
RepoCommitSection,
RepoPreview,
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
},
};
</script>
<template>
<div
class="multi-file"
:class="{
'is-collapsed': isCollapsed
}"
>
<repo-sidebar/>
<div
v-if="isCollapsed"
class="multi-file-edit-pane"
>
<repo-tabs />
<component
class="multi-file-edit-pane-content"
:is="currentBlobView"
/>
<repo-file-buttons />
</div>
<repo-commit-section />
</div>
</template>

View file

@ -1,85 +0,0 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
export default {
components: {
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
created() {
window.addEventListener('popstate', this.popHistoryState);
},
destroyed() {
window.removeEventListener('popstate', this.popHistoryState);
},
mounted() {
this.getTreeData();
},
computed: {
...mapState([
'loading',
'isRoot',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
...mapGetters([
'treeList',
'isCollapsed',
]),
},
methods: {
...mapActions([
'getTreeData',
'popHistoryState',
]),
},
};
</script>
<template>
<div class="ide-file-list">
<table class="table">
<thead>
<tr>
<th
v-if="isCollapsed"
>
</th>
<template v-else>
<th class="name multi-file-table-name">
Name
</th>
<th class="hidden-sm hidden-xs last-commit">
Last commit
</th>
<th class="hidden-xs last-update text-right">
Last update
</th>
</template>
</tr>
</thead>
<tbody>
<repo-previous-directory
v-if="!isRoot && treeList.length"
/>
<repo-loading-file
v-if="!treeList.length && loading"
v-for="n in 5"
:key="n"
/>
<repo-file
v-for="file in treeList"
:key="file.key"
:file="file"
/>
</tbody>
</table>
</div>
</template>

View file

@ -1,106 +0,0 @@
import Vue from 'vue';
import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue';
import store from './stores';
import Translate from '../vue_shared/translate';
function initRepo(el) {
if (!el) return null;
return new Vue({
el,
store,
components: {
repo: Repo,
},
methods: {
...mapActions([
'setInitialData',
]),
},
created() {
const data = el.dataset;
this.setInitialData({
project: {
id: data.projectId,
name: data.projectName,
url: data.projectUrl,
},
endpoints: {
rootEndpoint: data.url,
newMergeRequestUrl: data.newMergeRequestUrl,
rootUrl: data.rootUrl,
},
canCommit: convertPermissionToBoolean(data.canCommit),
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
currentRef: data.ref,
path: data.currentPath,
currentBranch: data.currentBranch,
isRoot: convertPermissionToBoolean(data.root),
isInitialRoot: convertPermissionToBoolean(data.root),
});
},
render(createElement) {
return createElement('repo');
},
});
}
function initRepoEditButton(el) {
return new Vue({
el,
store,
components: {
repoEditButton: RepoEditButton,
},
render(createElement) {
return createElement('repo-edit-button');
},
});
}
function initNewDropdown(el) {
return new Vue({
el,
store,
components: {
newDropdown,
},
render(createElement) {
return createElement('new-dropdown');
},
});
}
function initNewBranchForm() {
const el = document.querySelector('.js-new-branch-dropdown');
if (!el) return null;
return new Vue({
el,
components: {
newBranchForm,
},
store,
render(createElement) {
return createElement('new-branch-form');
},
});
}
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
Vue.use(Translate);
initRepo(repo);
initRepoEditButton(editButton);
initNewBranchForm();
initNewDropdown(newDropdownHolder);

View file

@ -1,146 +0,0 @@
import Vue from 'vue';
import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
export const discardAllChanges = ({ commit, getters, dispatch }) => {
const changedFiles = getters.changedFiles;
changedFiles.forEach((file) => {
commit(types.DISCARD_FILE_CHANGES, file);
if (file.tempFile) {
dispatch('closeFile', { file, force: true });
}
});
};
export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', { file }));
};
export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
const changedFiles = getters.changedFiles;
if (changedFiles.length && !force) {
commit(types.TOGGLE_DISCARD_POPUP, true);
} else {
commit(types.TOGGLE_EDIT_MODE);
commit(types.TOGGLE_DISCARD_POPUP, false);
dispatch('toggleBlobView');
if (!state.editMode) {
dispatch('discardAllChanges');
}
}
};
export const toggleBlobView = ({ commit, state }) => {
if (state.editMode) {
commit(types.SET_EDIT_MODE);
} else {
commit(types.SET_PREVIEW_MODE);
}
};
export const checkCommitStatus = ({ state }) => service.getBranchData(
state.project.id,
state.currentBranch,
)
.then((data) => {
const { id } = data.commit;
if (state.currentRef !== id) {
return true;
}
return false;
})
.catch(() => flash('Error checking branch data. Please try again.'));
export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
service.commit(state.project.id, payload)
.then((data) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message);
return;
}
const lastCommit = {
commit_path: `${state.project.url}/commit/${data.id}`,
commit: {
message: data.message,
authored_date: data.committed_date,
},
};
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) {
dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
} else {
commit(types.SET_COMMIT_REF, data.id);
getters.changedFiles.forEach((entry) => {
commit(types.SET_LAST_COMMIT_DATA, {
entry,
lastCommit,
});
});
dispatch('discardAllChanges');
dispatch('closeAllFiles');
dispatch('toggleEditMode');
window.scrollTo(0, 0);
}
})
.catch(() => flash('Error committing changes. Please try again.'));
export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
if (type === 'tree') {
dispatch('createTempTree', name);
} else if (type === 'blob') {
dispatch('createTempFile', {
tree: state,
name,
base64,
content,
});
}
};
export const popHistoryState = ({ state, dispatch, getters }) => {
const treeList = getters.treeList;
const tree = treeList.find(file => file.url === state.previousUrl);
if (!tree) return;
if (tree.type === 'tree') {
dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
}
};
export const scrollToTab = () => {
Vue.nextTick(() => {
const tabs = document.getElementById('tabs');
if (tabs) {
const tabEl = tabs.querySelector('.active .repo-tab');
tabEl.focus();
}
});
};
export * from './actions/tree';
export * from './actions/file';
export * from './actions/branch';

View file

@ -1,20 +0,0 @@
import service from '../../services';
import * as types from '../mutation_types';
import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
state.project.id,
{
branch,
ref: state.currentBranch,
},
).then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(state.currentBranch, branchName);
pushState(url);
commit(types.SET_CURRENT_BRANCH, branchName);
});

View file

@ -1,163 +0,0 @@
import { visitUrl } from '../../../lib/utils/url_utility';
import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
import {
pushState,
setPageTitle,
findEntry,
createTemp,
createOrMergeEntry,
} from '../utils';
export const getTreeData = (
{ commit, state, dispatch },
{ endpoint = state.endpoints.rootEndpoint, tree = state } = {},
) => {
commit(types.TOGGLE_LOADING, tree);
service.getTreeData(endpoint)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
const prevLastCommitPath = tree.lastCommitPath;
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
dispatch('updateDirectoryData', { data, tree });
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
commit(types.TOGGLE_LOADING, tree);
if (prevLastCommitPath !== null) {
dispatch('getLastCommitData', tree);
}
pushState(endpoint);
})
.catch(() => {
flash('Error loading tree data. Please try again.');
commit(types.TOGGLE_LOADING, tree);
});
};
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
if (tree.opened) {
// send empty data to clear the tree
const data = { trees: [], blobs: [], submodules: [] };
pushState(tree.parentTreeUrl);
commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
dispatch('updateDirectoryData', { data, tree });
} else {
commit(types.SET_PREVIOUS_URL, endpoint);
dispatch('getTreeData', { endpoint, tree });
}
commit(types.TOGGLE_TREE_OPEN, tree);
};
export const clickedTreeRow = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', {
endpoint: row.url,
tree: row,
});
} else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row);
visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row);
} else {
dispatch('getFileData', row);
}
};
export const createTempTree = ({ state, commit, dispatch }, name) => {
let tree = state;
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
dirNames.forEach((dirName) => {
const foundEntry = findEntry(tree, 'tree', dirName);
if (!foundEntry) {
const tmpEntry = createTemp({
name: dirName,
path: tree.path,
type: 'tree',
level: tree.level !== undefined ? tree.level + 1 : 0,
});
commit(types.CREATE_TMP_TREE, {
parent: tree,
tmpEntry,
});
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
tree = tmpEntry;
} else {
tree = foundEntry;
}
});
if (tree.tempFile) {
dispatch('createTempFile', {
tree,
name: '.gitkeep',
});
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (tree.lastCommitPath === null || getters.isCollapsed) return;
service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
.then((data) => {
data.forEach((lastCommit) => {
const entry = findEntry(tree, lastCommit.type, lastCommit.file_name);
if (entry) {
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
}
});
dispatch('getLastCommitData', tree);
})
.catch(() => flash('Error fetching log data.'));
};
export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
const level = tree.level !== undefined ? tree.level + 1 : 0;
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
const createEntry = (entry, type) => createOrMergeEntry({
tree,
entry,
level,
type,
parentTreeUrl,
});
const formattedData = [
...data.trees.map(t => createEntry(t, 'tree')),
...data.submodules.map(m => createEntry(m, 'submodule')),
...data.blobs.map(b => createEntry(b, 'blob')),
];
commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
};

View file

@ -1,40 +0,0 @@
import _ from 'underscore';
/*
Takes the multi-dimensional tree and returns a flattened array.
This allows for the table to recursively render the table rows but keeps the data
structure nested to make it easier to add new files/directories.
*/
export const treeList = (state) => {
const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
return _.chain(state.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
};
export const changedFiles = state => state.openFiles.filter(file => file.changed);
export const activeFile = state => state.openFiles.find(file => file.active);
export const activeFileExtension = (state) => {
const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : '';
};
export const isCollapsed = state => !!state.openFiles.length;
export const canEditFile = (state) => {
const currentActiveFile = activeFile(state);
const openedFiles = state.openFiles;
return state.canCommit &&
state.onTopOfBranch &&
openedFiles.length &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);

Some files were not shown because too many files have changed in this diff Show more