diff --git a/.gitignore b/.gitignore index 7a7b5c9393..3e30fb8cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,6 @@ public/assets/ public/uploads.* public/uploads/ rails_best_practices_output.html -tags +/tags tmp/ vendor/bundle/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..ddf4e31204 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,67 @@ +# This file is generated by GitLab CI +before_script: + - ./scripts/prepare_build.sh + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - cp config/gitlab.yml.example config/gitlab.yml + - touch log/application.log + - touch log/test.log + - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" + - bundle exec rake db:create RAILS_ENV=test + +spec:feature: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature + tags: + - ruby + - mysql + +spec:api: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api + tags: + - ruby + - mysql + +spec:other: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other + tags: + - ruby + - mysql + +spinach:project: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project + tags: + - ruby + - mysql + +spinach:other: + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other + tags: + - ruby + - mysql + +teaspoon: + script: + - RAILS_ENV=test bundle exec teaspoon + tags: + - ruby + - mysql + +rubocop: + script: + - bundle exec rubocop + tags: + - ruby + - mysql + +brakeman: + script: + - bundle exec rake brakeman + tags: + - ruby + - mysql diff --git a/.rspec b/.rspec index 4e1e0d2f72..35f4d7441e 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,2 @@ --color +--format Fuubar diff --git a/.rubocop.yml b/.rubocop.yml index 03b78d6884..ea4d365761 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -954,7 +954,7 @@ Lint/Void: Rails/ActionFilter: Description: 'Enforces consistent use of action filter methods.' - Enabled: false + Enabled: true Rails/DefaultScope: Description: 'Checks if the argument passed to default_scope is a block.' @@ -993,8 +993,6 @@ Rails/Validation: AllCops: RunRailsCops: true Exclude: - - 'spec/**/*' - - 'features/**/*' - 'vendor/**/*' - 'db/**/*' - 'tmp/**/*' diff --git a/CHANGELOG b/CHANGELOG index 1aab904f11..0f4928e852 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,17 +1,334 @@ Please view this file on the master branch, on stable branches it's out of date. -v 7.11.0 (unreleased) +v 7.14.2 + - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu) + +v 7.14.1 + - Only include base URL in OmniAuth full_host parameter (Stan Hu) + - Fix Error 500 in API when accessing a group that has an avatar (Stan Hu) + - Fix "Reload with full diff" URL button in compare branch view (Stan Hu) + - Improve abuse reports management from admin area + +v 7.14.0 + - Fix bug where non-project members of the target project could set labels on new merge requests. + - Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller) + - Fix redirection after sign in when using auto_sign_in_with_provider + - Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu) + - Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu) + - Provide more feedback what went wrong if HipChat service failed test (Stan Hu) + - Fix bug where backslashes in inline diffs could be dropped (Stan Hu) + - Disable turbolinks when linking to Bitbucket import status (Stan Hu) + - Fix broken code import and display error messages if something went wrong with creating project (Stan Hu) + - Fix corrupted binary files when using API files endpoint (Stan Hu) + - Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu) + - Show incompatible projects in Bitbucket import status (Stan Hu) + - Fix coloring of diffs on MR Discussion-tab (Gert Goet) + - Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu) + - Fix errors deleting and creating branches with encoded slashes (Stan Hu) + - Always add current user to autocomplete controller to support filter by "Me" (Stan Hu) + - Fix multi-line syntax highlighting (Stan Hu) + - Fix network graph when branch name has single quotes (Stan Hu) + - Add "Confirm user" button in user admin page (Stan Hu) + - Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu) + - Add support for Unicode filenames in relative links (Hiroyuki Sato) + - Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki) + - Fix commit data retrieval when branch name has single quotes (Stan Hu) + - Check that project was actually created rather than just validated in import:repos task (Stan Hu) + - Fix full screen mode for snippet comments (Daniel Gerhardt) + - Fix 404 error in files view after deleting the last file in a repository (Stan Hu) + - Fix the "Reload with full diff" URL button (Stan Hu) + - Fix label read access for unauthenticated users (Daniel Gerhardt) + - Fix access to disabled features for unauthenticated users (Daniel Gerhardt) + - Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu) + - Fix file upload dialog for comment editing (Daniel Gerhardt) + - Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu) + - Return comments in created order in merge request API (Stan Hu) + - Disable internal issue tracker controller if external tracker is used (Stan Hu) + - Expire Rails cache entries after two weeks to prevent endless Redis growth + - Add support for destroying project milestones (Stan Hu) + - Allow custom backup archive permissions + - Add project star and fork count, group avatar URL and user/group web URL attributes to API + - Show who last edited a comment if it wasn't the original author + - Send notification to all participants when MR is merged. + - Add ability to manage user email addresses via the API. + - Show buttons to add license, changelog and contribution guide if they're missing. + - Tweak project page buttons. + - Disabled autocapitalize and autocorrect on login field (Daryl Chan) + - Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis) + - Update gravatar link on profile page to link to configured gravatar host (Ben Bodenmiller) + - Remove redis-store TTL monkey patch + - Add support for CI skipped status + - Fetch code from forks to refs/merge-requests/:id/head when merge request created + - Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg) + - Add "Check out branch" button to the MR page. + - Improve MR merge widget text and UI consistency. + - Improve text in MR "How To Merge" modal. + - Cache all events + - Order commits by date when comparing branches + - Fix bug causing error when the target branch of a symbolic ref was deleted + - Include branch/tag name in archive file and directory name + - Add dropzone upload progress + - Add a label for merged branches on branches page (Florent Baldino) + - Detect .mkd and .mkdn files as markdown (Ben Boeckel) + - Fix: User search feature in admin area does not respect filters + - Set max-width for README, issue and merge request description for easier read on big screens + - Update Flowdock integration to support new Flowdock API (Boyan Tabakov) + - Remove author from files view (Sven Strickroth) + - Fix infinite loop when SAML was incorrectly configured. + +v 7.13.5 + - Satellites reverted + +v 7.13.4 + - Allow users to send abuse reports + +v 7.13.3 + - Fix bug causing Bitbucket importer to crash when OAuth application had been removed. + - Allow users to send abuse reports + - Remove satellites + - Link username to profile on Group Members page (Tom Webster) + +v 7.13.2 + - Fix randomly failed spec + - Create project services on Project creation + - Add admin_merge_request ability to Developer level and up + - Fix Error 500 when browsing projects with no HEAD (Stan Hu) + - Fix labels / assignee / milestone for the merge requests when issues are disabled + - Show the first tab automatically on MergeRequests#new + - Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt) + - Fix Gmail Actions + +v 7.13.1 + - Fix: Label modifications are not reflected in existing notes and in the issue list + - Fix: Label not shown in the Issue list, although it's set through web interface + - Fix: Group/project references are linked incorrectly + - Improve documentation + - Fix of migration: Check if session_expire_delay column exists before adding the column + - Fix: ActionView::Template::Error + - Fix: "Create Merge Request" isn't always shown in event for newly pushed branch + - Fix bug causing "Remove source-branch" option not to work for merge requests from the same project. + - Render Note field hints consistently for "new" and "edit" forms + +v 7.13.0 + - Remove repository graph log to fix slow cache updates after push event (Stan Hu) + - Only enable HSTS header for HTTPS and port 443 (Stan Hu) + - Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu) + - Fix redirection to home page URL for unauthorized users (Daniel Gerhardt) + - Add branch switching support for graphs (Daniel Gerhardt) + - Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt) + - Remove link leading to a 404 error in Deploy Keys page (Stan Hu) + - Add support for unlocking users in admin settings (Stan Hu) + - Add Irker service configuration options (Stan Hu) + - Fix order of issues imported from GitHub (Hiroyuki Sato) + - Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart) + - Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI + - Add `two_factor_enabled` field to admin user API (Stan Hu) + - Fix invalid timestamps in RSS feeds (Rowan Wookey) + - Fix downloading of patches on public merge requests when user logged out (Stan Hu) + - Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu) + - Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu) + - Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu) + - Support commenting on diffs in side-by-side mode (Stan Hu) + - Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu) + - Return 40x error codes if branch could not be deleted in UI (Stan Hu) + - Remove project visibility icons from dashboard projects list + - Rename "Design" profile settings page to "Preferences". + - Allow users to customize their default Dashboard page. + - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8 + - Admin can edit and remove user identities + - Convert CRLF newlines to LF when committing using the web editor. + - API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged. + - Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled. + - Show a user's Two-factor Authentication status in the administration area. + - Explicit error when commit not found in the CI + - Improve performance for issue and merge request pages + - Users with guest access level can not set assignee, labels or milestones for issue and merge request + - Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels + - Better performance for pages with events list, issues list and commits list + - Faster automerge check and merge itself when source and target branches are in same repository + - Correctly show anonymous authorized applications under Profile > Applications. + - Query Optimization in MySQL. + - Allow users to be blocked and unblocked via the API + - Use native Postgres database cleaning during backup restore + - Redesign project page. Show README as default instead of activity. Move project activity to separate page + - Make left menu more hierarchical and less contextual by adding back item at top + - A fork can’t have a visibility level that is greater than the original project. + - Faster code search in repository and wiki. Fixes search page timeout for big repositories + - Allow administrators to disable 2FA for a specific user + - Add error message for SSH key linebreaks + - Store commits count in database (will populate with valid values only after first push) + - Rebuild cache after push to repository in background job + - Fix transferring of project to another group using the API. + +v 7.12.2 + - Correctly show anonymous authorized applications under Profile > Applications. + - Faster automerge check and merge itself when source and target branches are in same repository + - Audit log for user authentication + - Allow custom label to be set for authentication providers. + +v 7.12.1 + - Fix error when deleting a user who has projects (Stan Hu) + - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) + - Add SAML to list of social_provider (Matt Firtion) + - Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets) + - Fix closed merge request scope at milestone page (Dmitriy Zaporozhets) + - Revert merge request states renaming + - Fix hooks for web based events with external issue references (Daniel Gerhardt) + - Improve performance for issue and merge request pages + - Compress database dumps to reduce backup size + +v 7.12.0 + - Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu) + - Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu) + - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) + - Update oauth button logos for Twitter and Google to recommended assets + - Update browser gem to version 0.8.0 for IE11 support (Stan Hu) + - Fix timeout when rendering file with thousands of lines. + - Add "Remember me" checkbox to LDAP signin form. + - Add session expiration delay configuration through UI application settings + - Don't notify users mentioned in code blocks or blockquotes. + - Omit link to generate labels if user does not have access to create them (Stan Hu) + - Show warning when a comment will add 10 or more people to the discussion. + - Disable changing of the source branch in merge request update API (Stan Hu) + - Shorten merge request WIP text. + - Add option to disallow users from registering any application to use GitLab as an OAuth provider + - Support editing target branch of merge request (Stan Hu) + - Refactor permission checks with issues and merge requests project settings (Stan Hu) + - Fix Markdown preview not working in Edit Milestone page (Stan Hu) + - Fix Zen Mode not closing with ESC key (Stan Hu) + - Allow HipChat API version to be blank and default to v2 (Stan Hu) + - Add file attachment support in Milestone description (Stan Hu) + - Fix milestone "Browse Issues" button. + - Set milestone on new issue when creating issue from index with milestone filter active. + - Make namespace API available to all users (Stan Hu) + - Add web hook support for note events (Stan Hu) + - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu) + - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu) + - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu) + - Fix git blame syntax highlighting when different commits break up lines (Stan Hu) + - Add "Resend confirmation e-mail" link in profile settings (Stan Hu) + - Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka) + - Disabled expansion of top/bottom blobs for new file diffs + - Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka) + - Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka) + - Use the user list from the target project in a merge request (Stan Hu) + - Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen) + - Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen) + - Fix new/empty milestones showing 100% completion value (Jonah Bishop) + - Add a note when an Issue or Merge Request's title changes + - Consistently refer to MRs as either Merged or Closed. + - Add Merged tab to MR lists. + - Prefix EmailsOnPush email subject with `[Git]`. + - Group project contributions by both name and email. + - Clarify navigation labels for Project Settings and Group Settings. + - Move user avatar and logout button to sidebar + - You can not remove user if he/she is an only owner of group + - User should be able to leave group. If not - show him proper message + - User has ability to leave project + - Add SAML support as an omniauth provider + - Allow to configure a URL to show after sign out + - Add an option to automatically sign-in with an Omniauth provider + - GitLab CI service sends .gitlab-ci.yml in each push call + - When remove project - move repository and schedule it removal + - Improve group removing logic + - Trigger create-hooks on backup restore task + - Add option to automatically link omniauth and LDAP identities + - Allow special character in users bio. I.e.: I <3 GitLab + +v 7.11.4 + - Fix missing bullets when creating lists + - Set rel="nofollow" on external links + +v 7.11.3 + - no changes + - Fix upgrader script (Martins Polakovs) + +v 7.11.2 + - no changes + +v 7.11.1 + - no changes + +v 7.11.0 + - Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger) + - Get editing comments to work in Chrome 43 again. + - Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu) + - Don't show duplicate deploy keys + - Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger) + - Make the first branch pushed to an empty repository the default HEAD (Stan Hu) + - Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu) + - Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu) + - Add application setting to restrict user signups to e-mail domains (Stan Hu) + - Don't allow a merge request to be merged when its title starts with "WIP". + - Add a page title to every page. + - Allow primary email to be set to an email that you've already added. - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) - Ignore invalid lines in .gitmodules - - - - - - - - - - - - - - + - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu) + - Redirect to sign in page after signing out. + - Fix "Hello @username." references not working by no longer allowing usernames to end in period. + - Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu) + - Improve project page UI + - Fix broken file browsing with relative submodule in personal projects (Stan Hu) + - Add "Reply quoting selected text" shortcut key (`r`) + - Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention. + - Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention. + - When use change branches link at MR form - save source branch selection instead of target one + - Improve handling of large diffs + - Added GitLab Event header for project hooks + - Add Two-factor authentication (2FA) for GitLab logins + - Show Atom feed buttons everywhere where applicable. + - Add project activity atom feed. + - Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits. + - Explain how to get a new password reset token in welcome emails + - Include commit comments in MR from a forked project. + - Group milestones by title in the dashboard and all other issue views. + - Query issues, merge requests and milestones with their IID through API (Julien Bianchi) + - Add default project and snippet visibility settings to the admin web UI. + - Show incompatible projects in Google Code import status (Stan Hu) + - Fix bug where commit data would not appear in some subdirectories (Stan Hu) + - Task lists are now usable in comments, and will show up in Markdown previews. + - Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu) + - Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu) + - Protect OmniAuth request phase against CSRF. + - Don't send notifications to mentioned users that don't have access to the project in question. + - Add search issues/MR by number + - Move snippets UI to fluid layout + - Improve UI for sidebar. Increase separation between navigation and content + - Improve new project command options (Ben Bodenmiller) + - Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük) + - Prevent sending empty messages to HipChat (Chulki Lee) + - Improve UI for mobile phones on dashboard and project pages + - Add room notification and message color option for HipChat + - Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka) + - Add footnotes support to Markdown (Guillaume Delbergue) + - Add current_sign_in_at to UserFull REST api. + - Make Sidekiq MemoryKiller shutdown signal configurable + - Add "Create Merge Request" buttons to commits and branches pages and push event. + - Show user roles by comments. + - Fix automatic blocking of auto-created users from Active Directory. + - Call merge request web hook for each new commits (Arthur Gautier) + - Use SIGKILL by default in Sidekiq::MemoryKiller + - Fix mentioning of private groups. + - Add style for element in markdown + - Spin spinner icon next to "Checking for CI status..." on MR page. + - Fix reference links in dashboard activity and ATOM feeds. + - Ensure that the first added admin performs repository imports -v 7.10.0 (unreleased) +v 7.10.4 + - Fix migrations broken in 7.10.2 + - Make tags for GitLab installations running on MySQL case sensitive + - Get Gitorious importer to work again. + - Fix adding new group members from admin area + - Fix DB error when trying to tag a repository (Stan Hu) + - Fix Error 500 when searching Wiki pages (Stan Hu) + - Unescape branch names in compare commit (Stan Hu) + - Order commit comments chronologically in API. + +v 7.10.2 + - Fix CI links on MR page + +v 7.10.0 - Ignore submodules that are defined in .gitmodules but are checked in as directories. - Allow projects to be imported from Google Code. - Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger) @@ -37,7 +354,6 @@ v 7.10.0 (unreleased) - Allow HTML tags in Markdown input - Fix code unfold not working on Compare commits page (Stan Hu) - Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik) - - Include missing events and fix save functionality in admin service template settings form (Stan Hu) - Fix "Import projects from" button to show the correct instructions (Stan Hu) - Fix dots in Wiki slugs causing errors (Stan Hu) - Make maximum attachment size configurable via Application Settings (Stan Hu) @@ -89,13 +405,12 @@ v 7.10.0 (unreleased) - Ability to skip some items from backup (database, respositories or uploads) - Archive repositories in background worker. - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace. - - Project labels are now available over the API under the "tag_list" field (Cristian Medina) + - Project labels are now available over the API under the "tag_list" field (Cristian Medina) - Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz) - Fix and improve help rendering (Sullivan Sénéchal) - Fix final line in EmailsOnPush email diff being rendered as error. - - Authometic setup GitLab CI project for forks if origin project has GitLab CI enabled - Prevent duplicate Buildkite service creation. - - Fix git over ssh errors 'fatal: protocol error: bad line length character' + - Fix git over ssh errors 'fatal: protocol error: bad line length character' - Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled - Bust group page project list cache when namespace name or path changes. - Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded @@ -104,7 +419,7 @@ v 7.10.0 (unreleased) - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) - Fix merge request comments on files with multiple commits - Fix Resource Owner Password Authentication Flow - + v 7.9.4 - Security: Fix project import URL regex to prevent arbitary local repos from being imported - Fixed issue where only 25 commits would load in file listings @@ -409,6 +724,12 @@ v 7.5.0 - Use secret token with GitLab internal API. - Add missing timestamps to 'members' table +v 7.4.5 + - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) + +v 7.4.4 + - No changes + v 7.4.3 - Fix raw snippets view - Fix security issue for member api diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3165b7379d..69abadb151 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,11 +29,9 @@ You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq ## Issue tracker -To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://about.gitlab.com/subscription/) and [consulting services](http://about.gitlab.com/consultancy/) are available from [GitLab.com](http://about.gitlab.com/). +To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/getting-help/). -The [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. - -Issues can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) or [github.com](https://github.com/gitlabhq/gitlabhq/issues). +The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple. @@ -63,13 +61,13 @@ Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint. -To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. +To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. ### Merge request guidelines If you can, please submit a merge request with the fix or improvements including tests. If you don't know how to fix the issue but can write a test that exposes the issue we will accept that as well. In general bug fixes that include a regression test are merged quickly while new features without proper tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: -1. Fork the project on GitLab Cloud +1. Fork the project into your personal space on GitLab.com 1. Create a feature branch 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Add your changes to the [CHANGELOG](CHANGELOG) @@ -86,7 +84,9 @@ If you can, please submit a merge request with the fix or improvements including 1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). 1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk. -The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as assisting subscribers with upgrade issues, the release of Enterprise Edition and the upgrade of GitLab Cloud. After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. +The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. +Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as regressions requiring patch releases. +After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. Please keep the change in a single MR **as small as possible**. If you want to contribute a large feature think very hard what the minimum viable change is. Can you split functionality? Can you only submit the backend/API code? Can you start with a very simple UI? Can you do part of the refactor? The increased reviewability of small MR's that leads to higher code quality is more important to us than having a minimal commit log. The smaller a MR is the more likely it is it will be merged (quickly), after that you can send more MR's to enhance it. @@ -160,20 +160,24 @@ If you add a dependency in GitLab (such as an operating system package) please c 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style#coffeescript) 1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) +1. [Database Migrations](doc/development/migration_style_guide.md) +1. [Documentation styleguide](doc_styleguide.md) 1. Interface text should be written subjectively instead of objectively. It should be the gitlab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing). This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com). ## Code of conduct + As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. -We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. -Instances of abusive, harassing, or otherwise unacceptable behavior can be -reported by emailing contact@gitlab.com +This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. -This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) +Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com + +This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 097a15a2af..57cf282ebb 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.2 +2.6.5 diff --git a/Gemfile b/Gemfile index 56fcd8d35f..fe05f0f9f6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,17 +1,10 @@ source "https://rubygems.org" -def darwin_only(require_as) - RUBY_PLATFORM.include?('darwin') && require_as -end +gem 'rails', '4.1.11' -def linux_only(require_as) - RUBY_PLATFORM.include?('linux') && require_as -end - -gem "rails", "~> 4.1.0" - -# Make links from text -gem 'rails_autolink', '~> 1.1' +# Specify a sprockets version due to security issue +# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY +gem 'sprockets', '~> 2.12.3' # Default values for AR models gem "default_value_for", "~> 3.0.0" @@ -20,37 +13,51 @@ gem "default_value_for", "~> 3.0.0" gem "mysql2", group: :mysql gem "pg", group: :postgres -# Auth +# Authentication libraries gem "devise", '3.2.4' gem "devise-async", '0.9.0' -gem 'omniauth', "~> 1.1.3" +gem 'omniauth', "~> 1.2.2" gem 'omniauth-google-oauth2' gem 'omniauth-twitter' gem 'omniauth-github' gem 'omniauth-shibboleth' -gem 'omniauth-kerberos' +gem 'omniauth-kerberos', group: :kerberos gem 'omniauth-gitlab' gem 'omniauth-bitbucket' +gem 'omniauth-saml', '~> 1.4.0' gem 'doorkeeper', '2.1.3' gem "rack-oauth2", "~> 1.0.5" +# Two-factor authentication +gem 'devise-two-factor' +gem 'rqrcode-rails3' +gem 'attr_encrypted', '1.3.4' + # Browser detection -gem "browser" +gem "browser", '~> 0.8.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.1.10' +gem "gitlab_git", '~> 7.2.15' # Ruby/Rack Git Smart-HTTP Server Handler +# GitLab fork with a lot of changes (improved thread-safety, better memory usage etc) +# For full list of changes see https://github.com/SaitoWu/grack/compare/master...gitlabhq:master gem 'gitlab-grack', '~> 2.0.2', require: 'grack' # LDAP Auth +# GitLab fork with several improvements to original library. For full list of changes +# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" # Git Wiki gem 'gollum-lib', '~> 4.0.2' # Language detection +# GitLab fork of linguist does not require pygments/python dependency. +# New version of original gem also dropped pygments support but it has strict +# dependency to unstable rugged version. We have internal issue for replacing +# fork with original gem when we meet on same rugged version - https://dev.gitlab.org/gitlab/gitlabhq/issues/2052. gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API @@ -78,7 +85,7 @@ gem "carrierwave" gem 'dropzonejs-rails' # for aws storage -gem "fog", "~> 1.14" +gem "fog", "~> 1.25.0" gem "unf" # Authorization @@ -87,20 +94,17 @@ gem "six" # Seed data gem "seed-fu" -# Markup pipeline for GitLab -gem 'html-pipeline-gitlab', '~> 0.1' - -# Markdown to HTML -gem "github-markup" - -# Required markup gems by github-markdown -gem 'redcarpet', '~> 3.2.3' +# Markdown and HTML processing +gem 'html-pipeline', '~> 1.11.0' +gem 'task_list', '1.0.2', require: 'task_list/railtie' +gem 'github-markup' +gem 'redcarpet', '~> 3.3.2' gem 'RedCloth' -gem 'rdoc', '~>3.6' -gem 'org-ruby', '= 0.9.12' -gem 'creole', '~>0.3.6' -gem 'wikicloth', '=0.8.1' -gem 'asciidoctor', '= 0.1.4' +gem 'rdoc', '~>3.6' +gem 'org-ruby', '= 0.9.12' +gem 'creole', '~>0.3.6' +gem 'wikicloth', '=0.8.1' +gem 'asciidoctor', '~> 1.5.2' # Diffs gem 'diffy', '~> 3.0.3' @@ -143,10 +147,10 @@ gem "redis-rails" gem 'tinder', '~> 1.9.2' # HipChat integration -gem "hipchat", "~> 1.4.0" +gem 'hipchat', '~> 1.5.0' # Flowdock integration -gem "gitlab-flowdock-git-hook", "~> 0.4.2" +gem "gitlab-flowdock-git-hook", "~> 1.0.1" # Gemnasium integration gem "gemnasium-gitlab-service", "~> 0.2" @@ -158,7 +162,7 @@ gem "slack-notifier", "~> 1.0.0" gem 'asana', '~> 0.0.6' # d3 -gem "d3_rails", "~> 3.1.4" +gem 'd3_rails', '~> 3.5.5' #cal-heatmap gem "cal-heatmap-rails", "~> 0.0.1" @@ -170,7 +174,7 @@ gem "underscore-rails", "~> 1.4.4" gem "sanitize", '~> 2.0' # Protect against bruteforcing -gem "rack-attack" +gem "rack-attack", '~> 4.3.0' # Ace editor gem 'ace-rails-ap' @@ -181,26 +185,26 @@ gem 'mousetrap-rails' # Detect and convert string character encoding gem 'charlock_holmes' -gem "sass-rails", '~> 4.0.2' +gem "sass-rails", '~> 4.0.5' gem "coffee-rails" gem "uglifier" -gem 'turbolinks' +gem 'turbolinks', '~> 2.5.0' gem 'jquery-turbolinks' -gem 'select2-rails' -gem 'jquery-atwho-rails', "~> 0.3.3" -gem "jquery-rails" -gem "jquery-ui-rails" -gem "jquery-scrollto-rails" -gem "raphael-rails", "~> 2.1.2" -gem 'bootstrap-sass', '~> 3.0' -gem "font-awesome-rails", '~> 4.2' -gem "gitlab_emoji", "~> 0.1" -gem "gon", '~> 5.0.0' -gem 'nprogress-rails' -gem 'request_store' -gem "virtus" gem 'addressable' +gem 'bootstrap-sass', '~> 3.0' +gem 'font-awesome-rails', '~> 4.2' +gem 'gitlab_emoji', '~> 0.1' +gem 'gon', '~> 5.0.0' +gem 'jquery-atwho-rails', '~> 1.0.0' +gem 'jquery-rails', '3.1.3' +gem 'jquery-scrollto-rails' +gem 'jquery-ui-rails' +gem 'nprogress-rails' +gem 'raphael-rails', '~> 2.1.2' +gem 'request_store' +gem 'select2-rails', '~> 3.5.9' +gem 'virtus' group :development do gem 'brakeman', require: false @@ -208,6 +212,7 @@ group :development do gem "letter_opener" gem 'quiet_assets', '~> 1.0.1' gem 'rack-mini-profiler', require: false + gem 'rerun', '~> 0.10.0' # Better errors handler gem 'better_errors' @@ -221,50 +226,42 @@ group :development do end group :development, :test do - gem 'coveralls', require: false - gem 'rubocop', '0.28.0', require: false - # gem 'rails-dev-tweaks' - gem 'spinach-rails' - gem "rspec-rails", '2.99' - gem "capybara", '~> 2.2.1' - gem "pry-rails" - gem "awesome_print" - gem "database_cleaner" - gem "launchy" + gem 'awesome_print' + gem 'byebug', platform: :mri + gem 'fuubar', '~> 2.0.0' + gem 'pry-rails' + + gem 'coveralls', '~> 0.8.2', require: false + gem 'database_cleaner', '~> 1.4.0' gem 'factory_girl_rails' + gem 'rspec-rails', '~> 3.3.0' + gem 'rubocop', '0.28.0', require: false + gem 'spinach-rails' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.3.0' # Generate Fake data - gem "ffaker" + gem 'ffaker', '~> 2.0.0' - # Guard - gem 'guard-rspec' - gem 'guard-spinach' + gem 'capybara', '~> 2.4.0' + gem 'capybara-screenshot', '~> 1.0.0' + gem 'poltergeist', '~> 1.6.0' - # Notification - gem 'rb-fsevent', require: darwin_only('rb-fsevent') - gem 'growl', require: darwin_only('growl') - gem 'rb-inotify', require: linux_only('rb-inotify') + gem 'teaspoon', '~> 1.0.0' + gem 'teaspoon-jasmine' - # PhantomJS driver for Capybara - gem 'poltergeist', '~> 1.5.1' - - gem 'jasmine', '2.0.2' - - gem "spring", '~> 1.3.1' - gem "spring-commands-rspec", '1.0.4' - gem "spring-commands-spinach", '1.0.0' - - gem "byebug" + gem 'spring', '~> 1.3.1' + gem 'spring-commands-rspec', '~> 1.0.0' + gem 'spring-commands-spinach', '~> 1.0.0' + gem 'spring-commands-teaspoon', '~> 0.0.2' end group :test do - gem "simplecov", require: false - gem "shoulda-matchers", "~> 2.7.0" - gem 'email_spec' - gem "webmock" + gem 'simplecov', require: false + gem 'shoulda-matchers', '~> 2.8.0', require: false + gem 'email_spec', '~> 1.6.0' + gem 'webmock', '~> 1.21.0' gem 'test_after_commit' end @@ -275,4 +272,3 @@ end gem "newrelic_rpm" gem 'octokit', '3.7.0' -gem "rugments" diff --git a/Gemfile.lock b/Gemfile.lock index 2fd59857be..d09d1f9dc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,33 +1,34 @@ GEM remote: https://rubygems.org/ specs: + CFPropertyList (2.3.1) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.1.9) - actionpack (= 4.1.9) - actionview (= 4.1.9) + actionmailer (4.1.11) + actionpack (= 4.1.11) + actionview (= 4.1.11) mail (~> 2.5, >= 2.5.4) - actionpack (4.1.9) - actionview (= 4.1.9) - activesupport (= 4.1.9) + actionpack (4.1.11) + actionview (= 4.1.11) + activesupport (= 4.1.11) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.9) - activesupport (= 4.1.9) + actionview (4.1.11) + activesupport (= 4.1.11) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.9) - activesupport (= 4.1.9) + activemodel (4.1.11) + activesupport (= 4.1.11) builder (~> 3.1) - activerecord (4.1.9) - activemodel (= 4.1.9) - activesupport (= 4.1.9) + activerecord (4.1.11) + activemodel (= 4.1.11) + activesupport (= 4.1.11) arel (~> 5.0.0) activeresource (4.0.0) activemodel (~> 4.0) activesupport (~> 4.0) rails-observers (~> 0.1.1) - activesupport (4.1.9) + activesupport (4.1.11) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -35,19 +36,21 @@ GEM tzinfo (~> 1.1) acts-as-taggable-on (3.5.0) activerecord (>= 3.2, < 5) - addressable (2.3.5) + addressable (2.3.8) annotate (2.6.0) activerecord (>= 2.3.0) rake (>= 0.8.7) arel (5.0.1.20140414130214) asana (0.0.6) activeresource (>= 3.2.3) - asciidoctor (0.1.4) + asciidoctor (1.5.2) ast (2.0.0) astrolabe (1.3.0) parser (>= 2.2.0.pre.3, < 3.0) + attr_encrypted (1.3.4) + encryptor (>= 1.3.0) attr_required (1.0.0) - autoprefixer-rails (5.1.6) + autoprefixer-rails (5.1.11) execjs json awesome_print (1.2.0) @@ -60,7 +63,7 @@ GEM erubis (>= 2.6.6) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootstrap-sass (3.3.3) + bootstrap-sass (3.3.4.1) autoprefixer-rails (>= 5.0.0.1) sass (>= 3.2.19) brakeman (3.0.1) @@ -73,18 +76,21 @@ GEM ruby_parser (~> 3.5.0) sass (~> 3.0) terminal-table (~> 1.4) - browser (0.7.2) + browser (0.8.0) builder (3.2.2) byebug (3.2.0) columnize (~> 0.8) debugger-linecache (~> 1.2) cal-heatmap-rails (0.0.1) - capybara (2.2.1) + capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + capybara-screenshot (1.0.9) + capybara (>= 1.0, < 3) + launchy carrierwave (0.9.0) activemodel (>= 3.2.0) activesupport (>= 3.2.0) @@ -96,30 +102,30 @@ GEM coderay (1.1.0) coercible (1.0.0) descendants_tracker (~> 0.0.1) - coffee-rails (4.0.1) + coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) - coffee-script (2.2.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.6.3) + coffee-script-source (1.9.1.1) colored (1.2) colorize (0.5.8) columnize (0.9.0) connection_pool (2.1.0) - coveralls (0.7.0) - multi_json (~> 1.3) - rest-client - simplecov (>= 0.7) - term-ansicolor - thor - crack (0.4.1) - safe_yaml (~> 0.9.0) + coveralls (0.8.2) + json (~> 1.8) + rest-client (>= 1.6.8, < 2) + simplecov (~> 0.10.0) + term-ansicolor (~> 1.3) + thor (~> 0.19.1) + crack (0.4.2) + safe_yaml (~> 1.0.0) creole (0.3.8) - d3_rails (3.1.10) + d3_rails (3.5.5) railties (>= 3.1.0) daemons (1.1.9) - database_cleaner (1.3.0) + database_cleaner (1.4.1) debug_inspector (0.0.2) debugger-linecache (1.2.0) default_value_for (3.0.0) @@ -133,25 +139,35 @@ GEM warden (~> 1.2.3) devise-async (0.9.0) devise (~> 3.2) + devise-two-factor (1.0.1) + activemodel + activesupport + attr_encrypted (~> 1.3.2) + devise (~> 3.2.4) + rails + rotp (~> 1.6.1) diff-lcs (1.2.5) diffy (3.0.3) docile (1.1.5) + domain_name (0.5.24) + unf (>= 0.0.5, < 1.0.0) doorkeeper (2.1.3) railties (>= 3.2) dotenv (0.9.0) - dropzonejs-rails (0.4.14) + dropzonejs-rails (0.7.1) rails (> 3.1) - email_spec (1.5.0) + email_spec (1.6.0) launchy (~> 2.1) mail (~> 2.2) + encryptor (1.3.0) enumerize (0.7.0) activesupport (>= 3.2) equalizer (0.0.8) erubis (2.7.0) escape_utils (0.2.4) eventmachine (1.0.4) - excon (0.32.1) - execjs (2.0.2) + excon (0.45.3) + execjs (2.5.2) expression_parser (0.9.0) factory_girl (4.3.0) activesupport (>= 3.0.0) @@ -163,31 +179,77 @@ GEM faraday_middleware (0.9.0) faraday (>= 0.7.4, < 0.9) fastercsv (1.5.5) - ffaker (1.22.1) - ffi (1.9.3) - fog (1.21.0) - fog-brightbox - fog-core (~> 1.21, >= 1.21.1) + ffaker (2.0.0) + ffi (1.9.8) + fission (0.5.0) + CFPropertyList (~> 2.2) + flowdock (0.7.0) + httparty (~> 0.7) + multi_json + fog (1.25.0) + fog-brightbox (~> 0.4) + fog-core (~> 1.25) fog-json + fog-profitbricks + fog-radosgw (>= 0.0.2) + fog-sakuracloud (>= 0.0.4) + fog-softlayer + fog-terremark + fog-vmfusion + fog-voxel + fog-xml (~> 0.1.1) + ipaddress (~> 0.5) nokogiri (~> 1.5, >= 1.5.11) - fog-brightbox (0.0.1) - fog-core + opennebula + fog-brightbox (0.7.1) + fog-core (~> 1.22) fog-json - fog-core (1.21.1) + inflecto (~> 0.0.2) + fog-core (1.30.0) builder - excon (~> 0.32) - formatador (~> 0.2.0) + excon (~> 0.45) + formatador (~> 0.2) mime-types net-scp (~> 1.1) net-ssh (>= 2.1.3) - fog-json (1.0.0) - multi_json (~> 1.0) + fog-json (1.0.2) + fog-core (~> 1.0) + multi_json (~> 1.10) + fog-profitbricks (0.0.3) + fog-core + fog-xml + nokogiri + fog-radosgw (0.0.4) + fog-core (>= 1.21.0) + fog-json + fog-xml (>= 0.0.1) + fog-sakuracloud (1.0.1) + fog-core + fog-json + fog-softlayer (0.4.6) + fog-core + fog-json + fog-terremark (0.1.0) + fog-core + fog-xml + fog-vmfusion (0.1.0) + fission + fog-core + fog-voxel (0.1.0) + fog-core + fog-xml + fog-xml (0.1.2) + fog-core + nokogiri (~> 1.5, >= 1.5.11) font-awesome-rails (4.2.0.0) railties (>= 3.2, < 5.0) foreman (0.63.0) dotenv (>= 0.7) thor (>= 0.13.6) - formatador (0.2.4) + formatador (0.2.5) + fuubar (2.0.0) + rspec (~> 3.0) + ruby-progressbar (~> 1.4) gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) gemojione (2.0.0) @@ -196,7 +258,8 @@ GEM racc github-markup (1.3.1) posix-spawn (~> 0.3.8) - gitlab-flowdock-git-hook (0.4.2.2) + gitlab-flowdock-git-hook (1.0.1) + flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json gitlab-grack (2.0.2) @@ -212,11 +275,11 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.1.10) + gitlab_git (7.2.15) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) - rugged (~> 0.21.2) + rugged (~> 0.22.2) gitlab_meta (7.0) gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) @@ -229,7 +292,7 @@ GEM github-markup (~> 1.3.1) gollum-grit_adapter (~> 0.1, >= 0.1.1) nokogiri (~> 1.6.4) - rouge (~> 1.7.4) + rouge (~> 1.9) sanitize (~> 2.1.0) stringex (~> 2.5.1) gon (5.0.1) @@ -248,20 +311,7 @@ GEM grape-entity (0.4.2) activesupport multi_json (>= 1.3.2) - growl (1.0.3) - guard (2.2.4) - formatador (>= 0.2.4) - listen (~> 2.1) - lumberjack (~> 1.0) - pry (>= 0.9.12) - thor (>= 0.18.1) - guard-rspec (4.2.0) - guard (>= 2.1.1) - rspec (>= 2.14, < 4.0) - guard-spinach (0.0.2) - guard (>= 1.1) - spinach - haml (4.0.5) + haml (4.0.7) tilt haml-rails (0.5.3) actionpack (>= 4.0.1) @@ -271,20 +321,17 @@ GEM hashie (2.1.2) highline (1.6.21) hike (1.2.3) - hipchat (1.4.0) + hipchat (1.5.0) httparty + mimemagic hitimes (1.2.2) html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) - html-pipeline-gitlab (0.2.0) - actionpack (~> 4) - gitlab_emoji (~> 0.1) - html-pipeline (~> 1.11.0) - mime-types - sanitize (~> 2.1) + http-cookie (1.0.2) + domain_name (~> 0.5) http_parser.rb (0.5.3) - httparty (0.13.0) + httparty (0.13.3) json (~> 1.8) multi_xml (>= 0.5.2) httpauth (0.2.1) @@ -292,14 +339,10 @@ GEM i18n (0.7.0) ice_cube (0.11.1) ice_nine (0.10.0) - jasmine (2.0.2) - jasmine-core (~> 2.0.0) - phantomjs - rack (>= 1.2.1) - rake - jasmine-core (2.0.0) - jquery-atwho-rails (0.3.3) - jquery-rails (3.1.0) + inflecto (0.0.2) + ipaddress (0.8.0) + jquery-atwho-rails (1.0.1) + jquery-rails (3.1.3) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) @@ -309,39 +352,42 @@ GEM turbolinks jquery-ui-rails (4.2.1) railties (>= 3.2.16) - json (1.8.2) + json (1.8.3) jwt (0.1.13) multi_json (>= 1.5) kaminari (0.15.1) actionpack (>= 3.0.0) activesupport (>= 3.0.0) kgio (2.9.2) - launchy (2.4.2) + launchy (2.4.3) addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - listen (2.3.1) - celluloid (>= 0.15.2) + listen (2.10.0) + celluloid (~> 0.16.0) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) - lumberjack (1.0.4) + macaddr (1.7.1) + systemu (~> 2.6.2) mail (2.6.3) mime-types (>= 1.16, < 3) method_source (0.8.2) mime-types (1.25.1) - mini_portile (0.6.1) + mimemagic (0.3.0) + mini_portile (0.6.2) minitest (5.3.5) mousetrap-rails (1.4.6) - multi_json (1.10.1) + multi_json (1.11.2) multi_xml (0.5.5) multipart-post (1.2.0) mysql2 (0.3.16) net-ldap (0.11) - net-scp (1.1.2) + net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (2.8.0) + net-ssh (2.9.2) + netrc (0.10.3) newrelic_rpm (3.9.4.245) - nokogiri (1.6.5) + nokogiri (1.6.6.2) mini_portile (~> 0.6.0) nprogress-rails (0.1.2.3) oauth (0.4.7) @@ -353,9 +399,9 @@ GEM rack (~> 1.2) octokit (3.7.0) sawyer (~> 0.6.0, >= 0.5.3) - omniauth (1.1.4) - hashie (>= 1.2, < 3) - rack + omniauth (1.2.2) + hashie (>= 1.2, < 4) + rack (~> 1.0) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -380,19 +426,25 @@ GEM omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) + omniauth-saml (1.4.1) + omniauth (~> 1.1) + ruby-saml (~> 1.0.0) omniauth-shibboleth (1.1.1) omniauth (>= 1.0.0) omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) + opennebula (4.12.1) + json + nokogiri + rbvmomi org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) parser (2.2.0.2) ast (>= 1.1, < 3.0) - pg (0.15.1) - phantomjs (1.9.2.0) - poltergeist (1.5.1) + pg (0.18.2) + poltergeist (1.6.0) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) @@ -409,10 +461,10 @@ GEM quiet_assets (1.0.2) railties (>= 3.1, < 5.0) racc (1.4.10) - rack (1.5.2) + rack (1.5.5) rack-accept (0.4.5) rack (>= 0.4) - rack-attack (4.2.0) + rack-attack (4.3.0) rack rack-cors (0.2.9) rack-mini-profiler (0.9.0) @@ -429,36 +481,38 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.1.9) - actionmailer (= 4.1.9) - actionpack (= 4.1.9) - actionview (= 4.1.9) - activemodel (= 4.1.9) - activerecord (= 4.1.9) - activesupport (= 4.1.9) + rails (4.1.11) + actionmailer (= 4.1.11) + actionpack (= 4.1.11) + actionview (= 4.1.11) + activemodel (= 4.1.11) + activerecord (= 4.1.11) + activesupport (= 4.1.11) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.9) + railties (= 4.1.11) sprockets-rails (~> 2.0) rails-observers (0.1.2) activemodel (~> 4.0) - rails_autolink (1.1.6) - rails (> 3.1) - railties (4.1.9) - actionpack (= 4.1.9) - activesupport (= 4.1.9) + railties (4.1.11) + actionpack (= 4.1.11) + activesupport (= 4.1.11) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) raindrops (0.13.0) rake (10.4.2) raphael-rails (2.1.2) - rb-fsevent (0.9.3) - rb-inotify (0.9.2) + rb-fsevent (0.9.4) + rb-inotify (0.9.5) ffi (>= 0.5.0) + rbvmomi (1.8.2) + builder + nokogiri (>= 1.4.1) + trollop rdoc (3.12.2) json (~> 1.4) - redcarpet (3.2.3) - redis (3.1.0) + redcarpet (3.3.2) + redis (3.2.1) redis-actionpack (4.0.0) actionpack (~> 4) redis-rack (~> 1.5.0) @@ -475,32 +529,42 @@ GEM redis-actionpack (~> 4) redis-activesupport (~> 4) redis-store (~> 1.1.0) - redis-store (1.1.4) + redis-store (1.1.6) redis (>= 2.2) request_store (1.0.5) - rest-client (1.6.7) - mime-types (>= 1.16) + rerun (0.10.0) + listen (~> 2.7, >= 2.7.3) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) rinku (1.7.3) - rouge (1.7.7) - rspec (2.99.0) - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) - rspec-collection_matchers (1.1.2) - rspec-expectations (>= 2.99.0.beta1) - rspec-core (2.99.2) - rspec-expectations (2.99.2) - diff-lcs (>= 1.1.3, < 2.0) - rspec-mocks (2.99.3) - rspec-rails (2.99.0) - actionpack (>= 3.0) - activemodel (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-collection_matchers - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) + rotp (1.6.1) + rouge (1.9.1) + rqrcode (0.4.2) + rqrcode-rails3 (0.1.7) + rqrcode (>= 0.4.2) + rspec (3.3.0) + rspec-core (~> 3.3.0) + rspec-expectations (~> 3.3.0) + rspec-mocks (~> 3.3.0) + rspec-core (3.3.1) + rspec-support (~> 3.3.0) + rspec-expectations (3.3.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-mocks (3.3.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-rails (3.3.2) + actionpack (>= 3.0, < 4.3) + activesupport (>= 3.0, < 4.3) + railties (>= 3.0, < 4.3) + rspec-core (~> 3.3.0) + rspec-expectations (~> 3.3.0) + rspec-mocks (~> 3.3.0) + rspec-support (~> 3.3.0) + rspec-support (3.3.0) rubocop (0.28.0) astrolabe (~> 1.3) parser (>= 2.2.0.pre.7, < 3.0) @@ -508,6 +572,9 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) ruby-progressbar (1.7.1) + ruby-saml (1.0.0) + nokogiri (>= 1.5.10) + uuid (~> 2.3) ruby2ruby (2.1.3) ruby_parser (~> 3.1) sexp_processor (~> 4.0) @@ -515,16 +582,15 @@ GEM sexp_processor (~> 4.1) rubyntlm (0.5.0) rubypants (0.2.0) - rugged (0.21.4) - rugments (1.0.0.beta6) - safe_yaml (0.9.7) + rugged (0.22.2) + safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) sass (3.2.19) - sass-rails (4.0.3) + sass-rails (4.0.5) railties (>= 4.0.0, < 5.0) - sass (~> 3.2.0) - sprockets (~> 2.8, <= 2.11.0) + sass (~> 3.2.2) + sprockets (~> 2.8, < 3.0) sprockets-rails (~> 2.0) sawyer (0.6.0) addressable (~> 2.3.5) @@ -532,14 +598,14 @@ GEM sdoc (0.3.20) json (>= 1.1.3) rdoc (~> 3.10) - seed-fu (2.3.1) - activerecord (>= 3.1, < 4.2) - activesupport (>= 3.1, < 4.2) - select2-rails (3.5.2) + seed-fu (2.3.5) + activerecord (>= 3.1, < 4.3) + activesupport (>= 3.1, < 4.3) + select2-rails (3.5.9.3) thor (~> 0.14) settingslogic (2.0.9) sexp_processor (4.4.5) - shoulda-matchers (2.7.0) + shoulda-matchers (2.8.0) activesupport (>= 3.0.0) sidekiq (3.3.0) celluloid (>= 0.16.0) @@ -552,11 +618,11 @@ GEM ice_cube (= 0.11.1) sidekiq (>= 3.0.0) simple_oauth (0.1.9) - simplecov (0.9.0) + simplecov (0.10.0) docile (~> 1.1.0) - multi_json - simplecov-html (~> 0.8.0) - simplecov-html (0.8.0) + json (~> 1.8) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) sinatra (1.4.4) rack (~> 1.4) rack-protection (~> 1.4) @@ -574,26 +640,35 @@ GEM capybara (>= 2.0.0) railties (>= 3) spinach (>= 0.4) - spring (1.3.3) + spring (1.3.6) spring-commands-rspec (1.0.4) spring (>= 0.9.1) spring-commands-spinach (1.0.0) spring (>= 0.9.1) - sprockets (2.11.0) + spring-commands-teaspoon (0.0.2) + spring (>= 0.9.1) + sprockets (2.12.4) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.2.4) + sprockets-rails (2.3.2) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) stamp (0.5.0) state_machine (1.2.0) stringex (2.5.2) + systemu (2.6.5) + task_list (1.0.2) + html-pipeline + teaspoon (1.0.2) + railties (>= 3.2.5, < 5) + teaspoon-jasmine (2.2.0) + teaspoon (>= 1.0.0) temple (0.6.7) - term-ansicolor (1.2.2) - tins (~> 0.8) + term-ansicolor (1.3.2) + tins (~> 1.0) terminal-table (1.4.5) test_after_commit (0.2.2) thin (1.6.1) @@ -615,8 +690,9 @@ GEM mime-types (~> 1.19) multi_json (~> 1.7) twitter-stream (~> 0.1) - tins (0.13.1) - turbolinks (2.0.0) + tins (1.5.4) + trollop (2.1.2) + turbolinks (2.5.3) coffee-rails twitter-stream (0.1.16) eventmachine (>= 0.12.8) @@ -630,13 +706,15 @@ GEM underscore-rails (1.4.4) unf (0.1.4) unf_ext - unf_ext (0.0.6) + unf_ext (0.0.7.1) unicorn (4.6.3) kgio (~> 2.6) rack raindrops (~> 0.7) unicorn-worker-killer (0.4.2) unicorn (~> 4) + uuid (2.3.8) + macaddr (~> 1.0) version_sorter (2.0.0) virtus (1.0.1) axiom-types (~> 0.0.5) @@ -645,10 +723,12 @@ GEM equalizer (~> 0.0.7) warden (1.2.3) rack (>= 1.0) - webmock (1.16.0) - addressable (>= 2.2.7) + webmock (1.21.0) + addressable (>= 2.3.6) crack (>= 0.3.2) - websocket-driver (0.3.3) + websocket-driver (0.5.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) wikicloth (0.8.1) builder expression_parser @@ -666,65 +746,64 @@ DEPENDENCIES addressable annotate (~> 2.6.0.beta2) asana (~> 0.0.6) - asciidoctor (= 0.1.4) + asciidoctor (~> 1.5.2) + attr_encrypted (= 1.3.4) awesome_print better_errors binding_of_caller bootstrap-sass (~> 3.0) brakeman - browser + browser (~> 0.8.0) byebug cal-heatmap-rails (~> 0.0.1) - capybara (~> 2.2.1) + capybara (~> 2.4.0) + capybara-screenshot (~> 1.0.0) carrierwave charlock_holmes coffee-rails colored - coveralls + coveralls (~> 0.8.2) creole (~> 0.3.6) - d3_rails (~> 3.1.4) - database_cleaner + d3_rails (~> 3.5.5) + database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) devise (= 3.2.4) devise-async (= 0.9.0) + devise-two-factor diffy (~> 3.0.3) doorkeeper (= 2.1.3) dropzonejs-rails - email_spec + email_spec (~> 1.6.0) enumerize factory_girl_rails - ffaker - fog (~> 1.14) + ffaker (~> 2.0.0) + fog (~> 1.25.0) font-awesome-rails (~> 4.2) foreman + fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) github-markup - gitlab-flowdock-git-hook (~> 0.4.2) + gitlab-flowdock-git-hook (~> 1.0.1) gitlab-grack (~> 2.0.2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.1.10) + gitlab_git (~> 7.2.15) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) gon (~> 5.0.0) grape (~> 0.6.1) grape-entity (~> 0.4.2) - growl - guard-rspec - guard-spinach haml-rails - hipchat (~> 1.4.0) - html-pipeline-gitlab (~> 0.1) + hipchat (~> 1.5.0) + html-pipeline (~> 1.11.0) httparty - jasmine (= 2.0.2) - jquery-atwho-rails (~> 0.3.3) - jquery-rails + jquery-atwho-rails (~> 1.0.0) + jquery-rails (= 3.1.3) jquery-scrollto-rails jquery-turbolinks jquery-ui-rails kaminari (~> 0.15.1) - launchy letter_opener minitest (~> 5.3.0) mousetrap-rails @@ -732,42 +811,41 @@ DEPENDENCIES newrelic_rpm nprogress-rails octokit (= 3.7.0) - omniauth (~> 1.1.3) + omniauth (~> 1.2.2) omniauth-bitbucket omniauth-github omniauth-gitlab omniauth-google-oauth2 omniauth-kerberos + omniauth-saml (~> 1.4.0) omniauth-shibboleth omniauth-twitter org-ruby (= 0.9.12) pg - poltergeist (~> 1.5.1) + poltergeist (~> 1.6.0) pry-rails quiet_assets (~> 1.0.1) - rack-attack + rack-attack (~> 4.3.0) rack-cors rack-mini-profiler rack-oauth2 (~> 1.0.5) - rails (~> 4.1.0) - rails_autolink (~> 1.1) + rails (= 4.1.11) raphael-rails (~> 2.1.2) - rb-fsevent - rb-inotify rdoc (~> 3.6) - redcarpet (~> 3.2.3) + redcarpet (~> 3.3.2) redis-rails request_store - rspec-rails (= 2.99) + rerun (~> 0.10.0) + rqrcode-rails3 + rspec-rails (~> 3.3.0) rubocop (= 0.28.0) - rugments sanitize (~> 2.0) - sass-rails (~> 4.0.2) + sass-rails (~> 4.0.5) sdoc seed-fu - select2-rails + select2-rails (~> 3.5.9) settingslogic - shoulda-matchers (~> 2.7.0) + shoulda-matchers (~> 2.8.0) sidekiq (~> 3.3) sidetiq (= 0.6.3) simplecov @@ -777,14 +855,19 @@ DEPENDENCIES slim spinach-rails spring (~> 1.3.1) - spring-commands-rspec (= 1.0.4) - spring-commands-spinach (= 1.0.0) + spring-commands-rspec (~> 1.0.0) + spring-commands-spinach (~> 1.0.0) + spring-commands-teaspoon (~> 0.0.2) + sprockets (~> 2.12.3) stamp state_machine + task_list (= 1.0.2) + teaspoon (~> 1.0.0) + teaspoon-jasmine test_after_commit thin tinder (~> 1.9.2) - turbolinks + turbolinks (~> 2.5.0) uglifier underscore-rails (~> 1.4.4) unf @@ -792,5 +875,8 @@ DEPENDENCIES unicorn-worker-killer version_sorter virtus - webmock + webmock (~> 1.21.0) wikicloth (= 0.8.1) + +BUNDLED WITH + 1.10.6 diff --git a/Guardfile b/Guardfile deleted file mode 100644 index 68ac3232b0..0000000000 --- a/Guardfile +++ /dev/null @@ -1,27 +0,0 @@ -# A sample Guardfile -# More info at https://github.com/guard/guard#readme - -guard 'rspec', cmd: "spring rspec", all_on_start: false, all_after_pass: false do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch(%r{^lib/api/(.+)\.rb$}) { |m| "spec/requests/api/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { "spec" } - - # Rails example - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch('config/routes.rb') { "spec/routing" } - watch('app/controllers/application_controller.rb') { "spec/controllers" } - - # Capybara request specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } -end - -guard 'spinach', command_prefix: 'spring' do - watch(%r|^features/(.*)\.feature|) - watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m| - "features/#{m[1]}#{m[2]}.feature" - end -end diff --git a/README.md b/README.md index 0563ceca40..52e12bb66a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ -# ![logo](https://about.gitlab.com/images/gitlab_logo.png) GitLab +# GitLab + +[![build status](https://ci.gitlab.com/projects/1/status.png?ref=master)](https://ci.gitlab.com/projects/1?ref=master) +[![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq) +[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) +[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) + +## Canonical source + +The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible. ## Open source software to collaborate on code -![Animated screenshots](https://about.gitlab.com/images/animated/compiled.gif) +To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). - Manage Git repositories with fine grained access controls that keep your code secure - Perform code reviews and enhance collaboration with merge requests @@ -13,25 +22,12 @@ ## Editions -There are two editions of GitLab. -*GitLab [Community Edition](https://about.gitlab.com/features/) (CE)* is available without any costs under an MIT license. +There are two editions of GitLab: -*GitLab Enterprise Edition (EE)* includes [extra features](https://about.gitlab.com/features/#compare) that are most useful for organizations with more than 100 users. -To get access to the EE and support please [become a subscriber](https://about.gitlab.com/pricing/). +- GitLab Community Edition (CE) is available freely under the MIT Expat license. +- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). -## Canonical source - -The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible. - -## Code status - -- [![build status](https://ci.gitlab.org/projects/1/status.png?ref=master)](https://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) - -- [![Build Status](https://semaphoreapp.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/243338/badge.png)](https://semaphoreapp.com/gitlabhq/gitlabhq) - -- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) - -- [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) +Included with the GitLab Omnibus Packages is [GitLab CI](https://about.gitlab.com/gitlab-ci/) that can easily build, test and deploy code. ## Website @@ -46,24 +42,40 @@ On [about.gitlab.com](https://about.gitlab.com/) you can find more information a ## Requirements -GitLab requires the following software: - -- Ubuntu/Debian/CentOS/RHEL -- Ruby (MRI) 2.0 or 2.1 -- Git 1.7.10+ -- Redis 2.0+ -- MySQL or PostgreSQL - Please see the [requirements documentation](doc/install/requirements.md) for system requirements and more information about the supported operating systems. ## Installation -The recommended way to install GitLab is using the provided [Omnibus packages](https://about.gitlab.com/downloads/). Compared to an installation from source, this is faster and less error prone. Just select your operating system, download the respective package (Debian or RPM) and install it using the system's package manager. +The recommended way to install GitLab is with the [Omnibus packages](https://about.gitlab.com/downloads/) on our package server. +Compared to an installation from source, this is faster and less error prone. +Just select your operating system, download the respective package (Debian or RPM) and install it using the system's package manager. There are various other options to install GitLab, please refer to the [installation page on the GitLab website](https://about.gitlab.com/installation/) for more information. You can access a new installation with the login **`root`** and password **`5iveL!fe`**, after login you are required to set a unique password. +## Install a development environment + +To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). +If you do not use the GitLab Development Kit you need to install and setup all the dependencies yourself, this is a lot of work and error prone. +One small thing you also have to do when installing it yourself is to copy the example development unicorn configuration file: + + cp config/unicorn.rb.example.development config/unicorn.rb + +Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). + +## Software stack + +GitLab is a Ruby on Rails application that runs on the following software: + +- Ubuntu/Debian/CentOS/RHEL +- Ruby (MRI) 2.1 +- Git 1.7.10+ +- Redis 2.0+ +- MySQL or PostgreSQL + +For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html). + ## Third-party applications There are a lot of [third-party applications integrating with GitLab](https://about.gitlab.com/applications/). These include GUI Git clients, mobile applications and API wrappers for various languages. @@ -74,17 +86,7 @@ Since 2011 a minor or major version of GitLab is released on the 22nd of every m ## Upgrading -For updating the Omnibus installation please see the [update documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md). For installations from source there is an [upgrader script](doc/update/upgrader.md) and there are [upgrade guides](doc/update) detailing all necessary commands to migrate to the next version. - -## Install a development environment - -To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). -If you do not use the GitLab Development Kit you need to install and setup all the dependencies yourself, this is a lot of work and error prone. -One small thing you also have to do when installing it yourself is to copy the example development unicorn configuration file: - - cp config/unicorn.rb.example.development config/unicorn.rb - -Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). +For upgrading information please see our [update page](https://about.gitlab.com/update/). ## Documentation diff --git a/VERSION b/VERSION index 9c51f30944..bd4a01e181 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.10.0 \ No newline at end of file +7.14.3 \ No newline at end of file diff --git a/app/assets/images/authbuttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png similarity index 100% rename from app/assets/images/authbuttons/bitbucket_64.png rename to app/assets/images/auth_buttons/bitbucket_64.png diff --git a/app/assets/images/auth_buttons/github_64.png b/app/assets/images/auth_buttons/github_64.png new file mode 100644 index 0000000000..182a1a3f73 Binary files /dev/null and b/app/assets/images/auth_buttons/github_64.png differ diff --git a/app/assets/images/auth_buttons/gitlab_64.png b/app/assets/images/auth_buttons/gitlab_64.png new file mode 100644 index 0000000000..99a40583b3 Binary files /dev/null and b/app/assets/images/auth_buttons/gitlab_64.png differ diff --git a/app/assets/images/auth_buttons/google_64.png b/app/assets/images/auth_buttons/google_64.png new file mode 100644 index 0000000000..fb64f8bee6 Binary files /dev/null and b/app/assets/images/auth_buttons/google_64.png differ diff --git a/app/assets/images/auth_buttons/twitter_64.png b/app/assets/images/auth_buttons/twitter_64.png new file mode 100644 index 0000000000..e3bd9169a3 Binary files /dev/null and b/app/assets/images/auth_buttons/twitter_64.png differ diff --git a/app/assets/images/authbuttons/github_64.png b/app/assets/images/authbuttons/github_64.png deleted file mode 100644 index dc7c03d100..0000000000 Binary files a/app/assets/images/authbuttons/github_64.png and /dev/null differ diff --git a/app/assets/images/authbuttons/gitlab_64.png b/app/assets/images/authbuttons/gitlab_64.png deleted file mode 100644 index 31281a1944..0000000000 Binary files a/app/assets/images/authbuttons/gitlab_64.png and /dev/null differ diff --git a/app/assets/images/authbuttons/google_64.png b/app/assets/images/authbuttons/google_64.png deleted file mode 100644 index 94a0e089c6..0000000000 Binary files a/app/assets/images/authbuttons/google_64.png and /dev/null differ diff --git a/app/assets/images/authbuttons/twitter_64.png b/app/assets/images/authbuttons/twitter_64.png deleted file mode 100644 index 5c9f14cb07..0000000000 Binary files a/app/assets/images/authbuttons/twitter_64.png and /dev/null differ diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico index bfb74960c4..3479cbbb46 100644 Binary files a/app/assets/images/favicon.ico and b/app/assets/images/favicon.ico differ diff --git a/app/assets/images/logo-white.png b/app/assets/images/logo-white.png deleted file mode 100644 index 917bcfcb7e..0000000000 Binary files a/app/assets/images/logo-white.png and /dev/null differ diff --git a/app/assets/images/logo.svg b/app/assets/images/logo.svg new file mode 100644 index 0000000000..c09785cb96 --- /dev/null +++ b/app/assets/images/logo.svg @@ -0,0 +1,26 @@ + + + + Slice 1 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/app/assets/images/logo_wordmark.svg b/app/assets/images/logo_wordmark.svg new file mode 100644 index 0000000000..a37fe1235c --- /dev/null +++ b/app/assets/images/logo_wordmark.svg @@ -0,0 +1,26 @@ + + + + Fill 1 + Group 24 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/msapplication-tile.png b/app/assets/images/msapplication-tile.png new file mode 100644 index 0000000000..58bbf2b20c Binary files /dev/null and b/app/assets/images/msapplication-tile.png differ diff --git a/app/assets/images/touch-icon-ipad-retina.png b/app/assets/images/touch-icon-ipad-retina.png new file mode 100644 index 0000000000..feb32b48ec Binary files /dev/null and b/app/assets/images/touch-icon-ipad-retina.png differ diff --git a/app/assets/images/touch-icon-ipad.png b/app/assets/images/touch-icon-ipad.png new file mode 100644 index 0000000000..a6ddc54350 Binary files /dev/null and b/app/assets/images/touch-icon-ipad.png differ diff --git a/app/assets/images/touch-icon-iphone-retina.png b/app/assets/images/touch-icon-iphone-retina.png new file mode 100644 index 0000000000..8bf7ccb753 Binary files /dev/null and b/app/assets/images/touch-icon-iphone-retina.png differ diff --git a/app/assets/images/touch-icon-iphone.png b/app/assets/images/touch-icon-iphone.png new file mode 100644 index 0000000000..87da550f8b Binary files /dev/null and b/app/assets/images/touch-icon-iphone.png differ diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index fda142293b..c263912b7e 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -16,7 +16,6 @@ #= require jquery.scrollTo #= require jquery.blockUI #= require jquery.turbolinks -#= require jquery.sticky-kit.min #= require turbolinks #= require autosave #= require bootstrap @@ -38,9 +37,10 @@ #= require shortcuts #= require shortcuts_navigation #= require shortcuts_dashboard_navigation -#= require shortcuts_issueable +#= require shortcuts_issuable #= require shortcuts_network #= require cal-heatmap +#= require jquery.nicescroll.min #= require_tree . window.slugify = (text) -> @@ -49,8 +49,6 @@ window.slugify = (text) -> window.ajaxGet = (url) -> $.ajax({type: "GET", url: url, dataType: "script"}) -window.showAndHide = (selector) -> - window.split = (val) -> return val.split( /,\s*/ ) @@ -92,15 +90,7 @@ window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) -> window.sanitize = (str) -> return str.replace(/<(?:.|\n)*?>/gm, '') -window.linkify = (str) -> - exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig - return str.replace(exp,"$1") - -window.simpleFormat = (str) -> - linkify(sanitize(str).replace(/\n/g, '
')) - window.unbindEvents = -> - $(document).unbind('scroll') $(document).off('scroll') window.shiftWindow = -> @@ -115,12 +105,23 @@ if location.hash window.addEventListener "hashchange", shiftWindow $ -> - # Click a .one_click_select field, select the contents - $(".one_click_select").on 'click', -> $(@).select() + $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") + + # Click a .js-select-on-focus field, select the contents + $(".js-select-on-focus").on "focusin", -> + # Prevent a mouseup event from deselecting the input + $(this).select().one 'mouseup', (e) -> + e.preventDefault() $('.remove-row').bind 'ajax:success', -> $(this).closest('li').fadeOut() + $('.js-remove-tr').bind 'ajax:before', -> + $(this).hide() + + $('.js-remove-tr').bind 'ajax:success', -> + $(this).closest('tr').fadeOut() + # Initialize select2 selects $('select.select2').select2(width: 'resolve', dropdownAutoWidth: true) @@ -132,17 +133,23 @@ $ -> ), 1 # Initialize tooltips - $('.has_tooltip').tooltip() - - # Bottom tooltip - $('.has_bottom_tooltip').tooltip(placement: 'bottom') + $('body').tooltip({ + selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a' + placement: (_, el) -> + $el = $(el) + if $el.attr('id') == 'js-shortcuts-home' + # Place the logo tooltip on the right when collapsed, bottom when expanded + $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom' + else + # Otherwise use the data-placement attribute, or 'bottom' if undefined + $el.data('placement') or 'bottom' + }) # Form submitter $('.trigger-submit').on 'change', -> $(@).parents('form').submit() - $("abbr.timeago").timeago() - $('.js-timeago').timeago() + $('abbr.timeago, .js-timeago').timeago() # Flash if (flash = $(".flash-container")).length > 0 @@ -163,9 +170,14 @@ $ -> $('.account-box').hover -> $(@).toggleClass('hover') # Commit show suppressed diff - $(".diff-content").on "click", ".supp_diff_link", -> - $(@).next('table').show() - $(@).remove() + $(document).on 'click', '.diff-content .js-show-suppressed-diff', -> + $container = $(@).parent() + $container.next('table').show() + $container.remove() + + $('.navbar-toggle').on 'click', -> + $('.header-content .title').toggle() + $('.header-content .navbar-collapse').toggle() # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> @@ -173,6 +185,7 @@ $ -> $(@).closest(".diff-file").find(".notes_holder").toggle() e.preventDefault() + $(document).off "click", '.js-confirm-danger' $(document).on "click", '.js-confirm-danger', (e) -> e.preventDefault() btn = $(e.target) @@ -181,14 +194,3 @@ $ -> new ConfirmDangerModal(form, text) new Aside() - -(($) -> - # Disable an element and add the 'disabled' Bootstrap class - $.fn.extend disable: -> - $(@).attr('disabled', 'disabled').addClass('disabled') - - # Enable an element and remove the 'disabled' Bootstrap class - $.fn.extend enable: -> - $(@).removeAttr('disabled').removeClass('disabled') - -)(jQuery) diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee new file mode 100644 index 0000000000..8318fe435b --- /dev/null +++ b/app/assets/javascripts/behaviors/requires_input.js.coffee @@ -0,0 +1,39 @@ +# Requires Input behavior +# +# When called on a form with input fields with the `required` attribute, the +# form's submit button will be disabled until all required fields have values. +# +#= require extensions/jquery +# +# ### Example Markup +# +#
+# +# +#
+# +$.fn.requiresInput = -> + $form = $(this) + $button = $('button[type=submit], input[type=submit]', $form) + + required = '[required=required]' + fieldSelector = "input#{required}, select#{required}, textarea#{required}" + + requireInput = -> + # Collect the input values of *all* required fields + values = _.map $(fieldSelector, $form), (field) -> field.value + + # Disable the button if any required fields are empty + if values.length && _.any(values, _.isEmpty) + $button.disable() + else + $button.enable() + + # Set initial button state + requireInput() + + $form.on 'change input', fieldSelector, requireInput + +# Triggered on standard document `ready` and on Turbolinks `page:load` events +$(document).on 'ready page:load', -> + $('form.js-requires-input').requiresInput() diff --git a/app/assets/javascripts/behaviors/taskable.js.coffee b/app/assets/javascripts/behaviors/taskable.js.coffee deleted file mode 100644 index ddce71c188..0000000000 --- a/app/assets/javascripts/behaviors/taskable.js.coffee +++ /dev/null @@ -1,21 +0,0 @@ -window.updateTaskState = (taskableType) -> - objType = taskableType.data - isChecked = $(this).prop("checked") - if $(this).is(":checked") - stateEvent = "task_check" - else - stateEvent = "task_uncheck" - - taskableUrl = $("form.edit-" + objType).first().attr("action") - taskableNum = taskableUrl.match(/\d+$/) - taskNum = 0 - $("li.task-list-item input:checkbox").each( (index, e) => - if e == this - taskNum = index + 1 - ) - - $.ajax - type: "PATCH" - url: taskableUrl - data: objType + "[state_event]=" + stateEvent + - "&" + objType + "[task_num]=" + taskNum diff --git a/app/assets/javascripts/blob/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee deleted file mode 100644 index 37a175fdbc..0000000000 --- a/app/assets/javascripts/blob/blob.js.coffee +++ /dev/null @@ -1,73 +0,0 @@ -class @BlobView - constructor: -> - # handle multi-line select - handleMultiSelect = (e) -> - [ first_line, last_line ] = parseSelectedLines() - [ line_number ] = parseSelectedLines($(this).attr("id")) - hash = "L#{line_number}" - - if e.shiftKey and not isNaN(first_line) and not isNaN(line_number) - if line_number < first_line - last_line = first_line - first_line = line_number - else - last_line = line_number - - hash = if first_line == last_line then "L#{first_line}" else "L#{first_line}-#{last_line}" - - setHash(hash) - e.preventDefault() - - # See if there are lines selected - # "#L12" and "#L34-56" supported - highlightBlobLines = (e) -> - [ first_line, last_line ] = parseSelectedLines() - - unless isNaN first_line - $("#tree-content-holder .highlight .line").removeClass("hll") - $("#LC#{line}").addClass("hll") for line in [first_line..last_line] - $.scrollTo("#L#{first_line}", offset: -50) unless e? - - # parse selected lines from hash - # always return first and last line (initialized to NaN) - parseSelectedLines = (str) -> - first_line = NaN - last_line = NaN - hash = str || window.location.hash - - if hash isnt "" - matches = hash.match(/\#?L(\d+)(\-(\d+))?/) - first_line = parseInt(matches?[1]) - last_line = parseInt(matches?[3]) - last_line = first_line if isNaN(last_line) - - [ first_line, last_line ] - - setHash = (hash) -> - hash = hash.replace(/^\#/, "") - nodes = $("#" + hash) - # if any nodes are using this id, they must be temporarily changed - # also, add a temporary div at the top of the screen to prevent scrolling - if nodes.length > 0 - scroll_top = $(document).scrollTop() - nodes.attr("id", "") - tmp = $("
") - .css({ position: "absolute", visibility: "hidden", top: scroll_top + "px" }) - .attr("id", hash) - .appendTo(document.body) - - window.location.hash = hash - - # restore the nodes - if nodes.length > 0 - tmp.remove() - nodes.attr("id", hash) - - # initialize multi-line select - $("#tree-content-holder .line-numbers a[id^=L]").on("click", handleMultiSelect) - - # Highlight the correct lines on load - highlightBlobLines() - - # Highlight the correct lines when the hash part of the URL changes - $(window).on("hashchange", highlightBlobLines) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 2e91a06daa..050888f9c1 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -11,7 +11,6 @@ class @EditBlob if ace_mode editor.getSession().setMode "ace/mode/" + ace_mode - disableButtonIfEmptyField "#commit_message", ".js-commit-button" $(".js-commit-button").click -> $("#file-content").val editor.getValue() $(".file-editor form").submit() diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee index ab8f98715e..1f36a53f19 100644 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ b/app/assets/javascripts/blob/new_blob.js.coffee @@ -11,7 +11,6 @@ class @NewBlob if ace_mode editor.getSession().setMode "ace/mode/" + ace_mode - disableButtonIfEmptyField "#commit_message", ".js-commit-button" $(".js-commit-button").click -> $("#file-content").val editor.getValue() $(".file-editor form").submit() diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee index 010a2b0e42..917228bd27 100644 --- a/app/assets/javascripts/branch-graph.js.coffee +++ b/app/assets/javascripts/branch-graph.js.coffee @@ -214,7 +214,7 @@ class @BranchGraph stroke: @colors[commit.space] "stroke-width": 2 ) - r.image(gon.relative_url_root + commit.author.icon, avatar_box_x, avatar_box_y, 20, 20) + r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20) r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr( "text-anchor": "start" font: "14px Monaco, monospace" diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 44d75bd694..4c4bc3d66e 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -25,6 +25,7 @@ class @Calendar 30 ] legendCellPadding: 3 + cellSize: $('.user-calendar').width() / 80 onClick: (date, count) -> formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() $.ajax diff --git a/app/assets/javascripts/confirm_danger_modal.js.coffee b/app/assets/javascripts/confirm_danger_modal.js.coffee index bb99edbd09..66e34dd4a0 100644 --- a/app/assets/javascripts/confirm_danger_modal.js.coffee +++ b/app/assets/javascripts/confirm_danger_modal.js.coffee @@ -8,11 +8,13 @@ class @ConfirmDangerModal submit = $('.js-confirm-danger-submit') submit.disable() + $('.js-confirm-danger-input').off 'input' $('.js-confirm-danger-input').on 'input', -> if rstrip($(@).val()) is project_path submit.enable() else submit.disable() + $('.js-confirm-danger-submit').off 'click' $('.js-confirm-danger-submit').on 'click', => @form.submit() diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index 069f91c30e..6d9b364cb8 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -31,6 +31,10 @@ class @Diff bottom: unfoldBottom offset: offset unfold: unfold + # indent is used to compensate for single space indent to fit + # '+' and '-' prepended to diff lines, + # see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 + indent: 1 $.get(link, params, (response) => target.parent().replaceWith(response) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 330ebac6f7..81e7379927 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -8,7 +8,6 @@ class Dispatcher initPageScripts: -> page = $('body').attr('data-page') - project_id = $('body').attr('data-project-id') unless page return false @@ -22,32 +21,27 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() - shortcut_handler = new ShortcutsIssueable() + shortcut_handler = new ShortcutsIssuable() new ZenMode() when 'projects:milestones:show' new Milestone() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() + new DropzoneInput($('.milestone-form')) when 'projects:compare:show' new Diff() when 'projects:issues:new','projects:issues:edit' - GitLab.GfmAutoComplete.setup() shortcut_handler = new ShortcutsNavigation() - new ZenMode() new DropzoneInput($('.issue-form')) - if page == 'projects:issues:new' - new IssuableForm($('.issue-form')) + new IssuableForm($('.issue-form')) when 'projects:merge_requests:new', 'projects:merge_requests:edit' - GitLab.GfmAutoComplete.setup() new Diff() shortcut_handler = new ShortcutsNavigation() - new ZenMode() new DropzoneInput($('.merge-request-form')) - if page == 'projects:merge_requests:new' - new IssuableForm($('.merge-request-form')) + new IssuableForm($('.merge-request-form')) when 'projects:merge_requests:show' new Diff() - shortcut_handler = new ShortcutsIssueable() + shortcut_handler = new ShortcutsIssuable() new ZenMode() when "projects:merge_requests:diffs" new Diff() @@ -55,7 +49,7 @@ class Dispatcher when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() MergeRequests.init() - when 'dashboard:show' + when 'dashboard:show', 'root:show' new Dashboard() new Activities() when 'dashboard:projects:starred' @@ -68,8 +62,9 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'projects:commits:show' shortcut_handler = new ShortcutsNavigation() + when 'projects:activity' + shortcut_handler = new ShortcutsNavigation() when 'projects:show' - new Activities() shortcut_handler = new ShortcutsNavigation() when 'groups:show' new Activities() @@ -87,7 +82,7 @@ class Dispatcher new TreeView() shortcut_handler = new ShortcutsNavigation() when 'projects:blob:show' - new BlobView() + new LineHighlighter() shortcut_handler = new ShortcutsNavigation() when 'projects:labels:new', 'projects:labels:edit' new Labels() @@ -128,14 +123,15 @@ class Dispatcher new ProjectNew() when 'show' new ProjectShow() - when 'issues', 'merge_requests' - new UsersSelect() when 'wikis' new Wikis() shortcut_handler = new ShortcutsNavigation() new ZenMode() new DropzoneInput($('.wiki-form')) - when 'snippets', 'labels', 'graphs' + when 'snippets' + shortcut_handler = new ShortcutsNavigation() + new ZenMode() if path[2] == 'show' + when 'labels', 'graphs' shortcut_handler = new ShortcutsNavigation() when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index fca2a290e2..a0dcaa8c27 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -8,22 +8,29 @@ class @DropzoneInput divAlert = "
" iconPaperclip = "" iconSpinner = "" + uploadProgress = $("
") btnAlert = "" project_uploads_path = window.project_uploads_path or null + markdown_preview_path = window.markdown_preview_path or null max_file_size = gon.max_file_size or 10 form_textarea = $(form).find("textarea.markdown-area") form_textarea.wrap "
" - form_textarea.bind 'paste', (event) => + form_textarea.on 'paste', (event) => handlePaste(event) + form_textarea.on "input", -> + hideReferencedUsers() + form_textarea.on "blur", -> + renderMarkdown() form_dropzone = $(form).find('.div-dropzone') form_dropzone.parent().addClass "div-dropzone-wrapper" form_dropzone.append divHover - $(".div-dropzone-hover").append iconPaperclip + form_dropzone.find(".div-dropzone-hover").append iconPaperclip form_dropzone.append divSpinner - $(".div-dropzone-spinner").append iconSpinner - $(".div-dropzone-spinner").css + form_dropzone.find(".div-dropzone-spinner").append iconSpinner + form_dropzone.find(".div-dropzone-spinner").append uploadProgress + form_dropzone.find(".div-dropzone-spinner").css "opacity": 0 "display": "none" @@ -45,16 +52,7 @@ class @DropzoneInput form.find(".md-write-holder").hide() form.find(".md-preview-holder").show() - preview = form.find(".js-md-preview") - mdText = form.find(".markdown-area").val() - if mdText.trim().length is 0 - preview.text "Nothing to preview." - else - preview.text "Loading..." - $.post($(this).data("url"), - md_text: mdText - ).success (previewData) -> - preview.html previewData + renderMarkdown() # Write button $(document).off "click", ".js-md-write-button" @@ -116,13 +114,18 @@ class @DropzoneInput $(".div-dropzone-alert").append btnAlert + errorMessage return + totaluploadprogress: (totalUploadProgress) -> + uploadProgress.text Math.round(totalUploadProgress) + "%" + return + sending: -> form_dropzone.find(".div-dropzone-spinner").css "opacity": 0.7 "display": "inherit" return - complete: -> + queuecomplete: -> + uploadProgress.text "" $(".dz-preview").remove() $(".markdown-area").trigger "input" $(".div-dropzone-spinner").css @@ -133,6 +136,40 @@ class @DropzoneInput child = $(dropzone[0]).children("textarea") + hideReferencedUsers = -> + referencedUsers = form.find(".referenced-users") + referencedUsers.hide() + + renderReferencedUsers = (users) -> + referencedUsers = form.find(".referenced-users") + + if referencedUsers.length + if users.length >= 10 + referencedUsers.show() + referencedUsers.find(".js-referenced-users-count").text users.length + else + referencedUsers.hide() + + renderMarkdown = -> + preview = form.find(".js-md-preview") + mdText = form.find(".markdown-area").val() + if mdText.trim().length is 0 + preview.text "Nothing to preview." + hideReferencedUsers() + else + preview.text "Loading..." + $.ajax( + type: "POST", + url: markdown_preview_path, + data: { + text: mdText + }, + dataType: "json" + ).success (data) -> + preview.html data.body + + renderReferencedUsers data.references.users + formatLink = (link) -> text = "[#{link.alt}](#{link.url})" text = "!#{text}" if link.is_image diff --git a/app/assets/javascripts/extensions/jquery.js.coffee b/app/assets/javascripts/extensions/jquery.js.coffee index 40fb6cb9fc..0a9db8eb5e 100644 --- a/app/assets/javascripts/extensions/jquery.js.coffee +++ b/app/assets/javascripts/extensions/jquery.js.coffee @@ -1,13 +1,11 @@ -$.fn.showAndHide = -> - $(@).show(). - delay(3000). - fadeOut() - -$.fn.enableButton = -> - $(@).removeAttr('disabled'). - removeClass('disabled') - -$.fn.disableButton = -> - $(@).attr('disabled', 'disabled'). - addClass('disabled') +# Disable an element and add the 'disabled' Bootstrap class +$.fn.extend disable: -> + $(@) + .attr('disabled', 'disabled') + .addClass('disabled') +# Enable an element and remove the 'disabled' Bootstrap class +$.fn.extend enable: -> + $(@) + .removeAttr('disabled') + .removeClass('disabled') diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 00d56ae5b4..7967892f85 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -2,19 +2,19 @@ window.GitLab ?= {} GitLab.GfmAutoComplete = - # private_token: '' dataSource: '' + # Emoji Emoji: - template: '
  • ${name} ${name}
  • ' + template: '
  • ${name} ${name}
  • ' # Team Members Members: - template: '
  • ${username} ${name}
  • ' + template: '
  • ${username} ${title}
  • ' # Issues and MergeRequests Issues: - template: '
  • ${id} ${title}
  • ' + template: '
  • ${id} ${title}
  • ' # Add GFM auto-completion to all input fields, that accept GFM input. setup: -> @@ -23,45 +23,58 @@ GitLab.GfmAutoComplete = # Emoji input.atwho at: ':' - tpl: @Emoji.template - callbacks: - before_save: (emojis) => - $.map emojis, (em) => name: em.name, insert: em.name+ ':', image: em.path + displayTpl: @Emoji.template + insertTpl: ':${name}:' # Team Members input.atwho at: '@' - tpl: @Members.template - search_key: 'search' + displayTpl: @Members.template + insertTpl: '${atwho-at}${username}' + searchKey: 'search' callbacks: - before_save: (members) => - $.map members, (m) => name: m.name, username: m.username, search: "#{m.username} #{m.name}" + beforeSave: (members) -> + $.map members, (m) -> + title = m.name + title += " (#{m.count})" if m.count + + username: m.username + title: sanitize(title) + search: sanitize("#{m.username} #{m.name}") input.atwho at: '#' alias: 'issues' - search_key: 'search' - tpl: @Issues.template + searchKey: 'search' + displayTpl: @Issues.template + insertTpl: '${atwho-at}${id}' callbacks: - before_save: (issues) -> - $.map issues, (i) -> id: i.iid, title: sanitize(i.title), search: "#{i.iid} #{i.title}" + beforeSave: (issues) -> + $.map issues, (i) -> + id: i.iid + title: sanitize(i.title) + search: "#{i.iid} #{i.title}" input.atwho at: '!' alias: 'mergerequests' - search_key: 'search' - tpl: @Issues.template + searchKey: 'search' + displayTpl: @Issues.template + insertTpl: '${atwho-at}${id}' callbacks: - before_save: (merges) -> - $.map merges, (m) -> id: m.iid, title: sanitize(m.title), search: "#{m.iid} #{m.title}" + beforeSave: (merges) -> + $.map merges, (m) -> + id: m.iid + title: sanitize(m.title) + search: "#{m.iid} #{m.title}" - input.one "focus", => + input.one 'focus', => $.getJSON(@dataSource).done (data) -> # load members - input.atwho 'load', "@", data.members + input.atwho 'load', '@', data.members # load issues - input.atwho 'load', "issues", data.issues + input.atwho 'load', 'issues', data.issues # load merge requests - input.atwho 'load', "mergerequests", data.mergerequests + input.atwho 'load', 'mergerequests', data.mergerequests # load emojis - input.atwho 'load', ":", data.emojis + input.atwho 'load', ':', data.emojis diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee new file mode 100644 index 0000000000..176d9cabef --- /dev/null +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -0,0 +1,22 @@ +#= require jquery.waitforimages + +class @IssuableContext + constructor: -> + new UsersSelect() + $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) + + $(".context .inline-update").on "change", "select", -> + $(this).submit() + $(".context .inline-update").on "change", ".js-assignee", -> + $(this).submit() + + $('.issuable-details').waitForImages -> + $('.issuable-affix').affix offset: + top: -> + @top = ($('.issuable-affix').offset().top - 70) + bottom: -> + @bottom = $('.footer').outerHeight(true) + $('.issuable-affix').on 'affix.bs.affix', -> + $(@).width($(@).outerWidth()) + .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> + $(@).width('') diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index abd58bcf97..48c249943f 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -1,5 +1,9 @@ class @IssuableForm constructor: (@form) -> + GitLab.GfmAutoComplete.setup() + new UsersSelect() + new ZenMode() + @titleField = @form.find("input[name*='[title]']") @descriptionField = @form.find("textarea[name*='[description]']") diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 4e2e6550eb..603a16da1c 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -1,24 +1,29 @@ +#= require jquery.waitforimages +#= require task_list + class @Issue constructor: -> - $('.edit-issue.inline-update input[type="submit"]').hide() - $(".context .inline-update").on "change", "select", -> - $(this).submit() - $(".context .inline-update").on "change", "#issue_assignee_id", -> - $(this).submit() + # Prevent duplicate event bindings + @disableTaskList() if $("a.btn-close").length - $("li.task-list-item input:checkbox").prop("disabled", false) + @initTaskList() - $('.task-list-item input:checkbox').off('change') - $('.task-list-item input:checkbox').change('issue', updateTaskState) + initTaskList: -> + $('.issue-details .js-task-list-container').taskList('enable') + $(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList - $('.issue-details').waitForImages -> - $('.issuable-affix').affix offset: - top: -> - @top = ($('.issuable-affix').offset().top - 70) - bottom: -> - @bottom = $('.footer').outerHeight(true) - $('.issuable-affix').on 'affix.bs.affix', -> - $(@).width($(@).outerWidth()) - .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> - $(@).width('') + disableTaskList: -> + $('.issue-details .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.issue-details .js-task-list-container' + + # TODO (rspeicher): Make the issue description inline-editable like a note so + # that we can re-use its form here + updateTaskList: -> + patchData = {} + patchData['issue'] = {'description': $('.js-task-list-field', this).val()} + + $.ajax + type: 'PATCH' + url: $('form.js-issuable-update').attr('action') + data: patchData diff --git a/app/assets/javascripts/labels.js.coffee b/app/assets/javascripts/labels.js.coffee index 1bc8840f9a..d05bacd749 100644 --- a/app/assets/javascripts/labels.js.coffee +++ b/app/assets/javascripts/labels.js.coffee @@ -1,7 +1,6 @@ class @Labels constructor: -> form = $('.label-form') - @setupLabelForm(form) @cleanBinding() @addBinding() @updateColorPreview() @@ -14,10 +13,6 @@ class @Labels $(document).off 'click', '.suggest-colors a' $(document).off 'input', 'input#label_color' - # Initializes the form to disable the save button if no color or title is entered - setupLabelForm: (form) -> - disableButtonIfAnyEmptyField form, '.form-control', form.find('.js-save-button') - # Updates the the preview color with the hex-color input updateColorPreview: => previewColor = $('input#label_color').val() diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee new file mode 100644 index 0000000000..e604e6025c --- /dev/null +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -0,0 +1,148 @@ +# LineHighlighter +# +# Handles single- and multi-line selection and highlight for blob views. +# +#= require jquery.scrollTo +# +# ### Example Markup +# +#
    +#
    +#
    +# 1 +# 2 +# 3 +# 4 +# 5 +#
    +#
    +#         
    +#           ...
    +#           ...
    +#           ...
    +#           ...
    +#           ...
    +#         
    +#       
    +#
    +#
    +# +class @LineHighlighter + # CSS class applied to highlighted lines + highlightClass: 'hll' + + # Internal copy of location.hash so we're not dependent on `location` in tests + _hash: '' + + # Initialize a LineHighlighter object + # + # hash - String URL hash for dependency injection in tests + constructor: (hash = location.hash) -> + @_hash = hash + + @bindEvents() + + unless hash == '' + range = @hashToRange(hash) + + if range[0] + @highlightRange(range) + + # Scroll to the first highlighted line on initial load + # Offset -50 for the sticky top bar, and another -100 for some context + $.scrollTo("#L#{range[0]}", offset: -150) + + bindEvents: -> + $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler + + # While it may seem odd to bind to the mousedown event and then throw away + # the click event, there is a method to our madness. + # + # If not done this way, the line number anchor will sometimes keep its + # active state even when the event is cancelled, resulting in an ugly border + # around the link and/or a persisted underline text decoration. + + $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) -> + event.preventDefault() + + clickHandler: (event) => + event.preventDefault() + + @clearHighlight() + + lineNumber = $(event.target).closest('a').data('line-number') + current = @hashToRange(@_hash) + + unless current[0] && event.shiftKey + # If there's no current selection, or there is but Shift wasn't held, + # treat this like a single-line selection. + @setHash(lineNumber) + @highlightLine(lineNumber) + else if event.shiftKey + if lineNumber < current[0] + range = [lineNumber, current[0]] + else + range = [current[0], lineNumber] + + @setHash(range[0], range[1]) + @highlightRange(range) + + # Unhighlight previously highlighted lines + clearHighlight: -> + $(".#{@highlightClass}").removeClass(@highlightClass) + + # Convert a URL hash String into line numbers + # + # hash - Hash String + # + # Examples: + # + # hashToRange('#L5') # => [5, null] + # hashToRange('#L5-15') # => [5, 15] + # hashToRange('#foo') # => [null, null] + # + # Returns an Array + hashToRange: (hash) -> + matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/) + + if matches && matches.length + first = parseInt(matches[1]) + last = if matches[2] then parseInt(matches[2]) else null + + [first, last] + else + [null, null] + + # Highlight a single line + # + # lineNumber - Line number to highlight + highlightLine: (lineNumber) => + $("#LC#{lineNumber}").addClass(@highlightClass) + + # Highlight all lines within a range + # + # range - Array containing the starting and ending line numbers + highlightRange: (range) -> + if range[1] + for lineNumber in [range[0]..range[1]] + @highlightLine(lineNumber) + else + @highlightLine(range[0]) + + # Set the URL hash string + setHash: (firstLineNumber, lastLineNumber) => + if lastLineNumber + hash = "#L#{firstLineNumber}-#{lastLineNumber}" + else + hash = "#L#{firstLineNumber}" + + @_hash = hash + @__setLocationHash__(hash) + + # Make the actual hash change in the browser + # + # This method is stubbed in tests. + __setLocationHash__: (value) -> + # We're using pushState instead of assigning location.hash directly to + # prevent the page from scrolling on the hashchange event + history.pushState({turbolinks: false, url: value}, document.title, value) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index fc75f14383..b21cb7904b 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -1,166 +1,59 @@ +#= require jquery.waitforimages +#= require task_list + +#= require merge_request_tabs + class @MergeRequest + # Initialize MergeRequest behavior + # + # Options: + # action - String, current controller action + # constructor: (@opts) -> - @initContextWidget() this.$el = $('.merge-request') - @diffs_loaded = if @opts.action == 'diffs' then true else false - @commits_loaded = false - this.activateTab(@opts.action) - - this.bindEvents() - - this.initMergeWidget() this.$('.show-all-commits').on 'click', => this.showAllCommits() - modal = $('#modal_merge_info').modal(show: false) + @initTabs() - disableButtonIfEmptyField '#commit_message', '.accept_merge_request' + # Prevent duplicate event bindings + @disableTaskList() if $("a.btn-close").length - $("li.task-list-item input:checkbox").prop("disabled", false) - - $('.merge-request-details').waitForImages -> - $('.issuable-affix').affix offset: - top: -> - @top = ($('.issuable-affix').offset().top - 70) - bottom: -> - @bottom = $('.footer').outerHeight(true) - $('.issuable-affix').on 'affix.bs.affix', -> - $(@).width($(@).outerWidth()) - .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> - $(@).width('') + @initTaskList() # Local jQuery finder $: (selector) -> this.$el.find(selector) - initContextWidget: -> - $('.edit-merge_request.inline-update input[type="submit"]').hide() - $(".context .inline-update").on "change", "select", -> - $(this).submit() - $(".context .inline-update").on "change", "#merge_request_assignee_id", -> - $(this).submit() - - initMergeWidget: -> - this.showState( @opts.current_status ) - - if this.$('.automerge_widget').length and @opts.check_enable - $.get @opts.url_to_automerge_check, (data) => - this.showState( data.merge_status ) - , 'json' - - if @opts.ci_enable - $.get @opts.url_to_ci_check, (data) => - this.showCiState data.status - if data.coverage - this.showCiCoverage data.coverage - , 'json' - - bindEvents: -> - this.$('.merge-request-tabs').on 'click', 'a', (event) => - a = $(event.currentTarget) - - href = a.attr('href') - History.replaceState {path: href}, document.title, href - - event.preventDefault() - - this.$('.merge-request-tabs').on 'click', 'li', (event) => - this.activateTab($(event.currentTarget).data('action')) - - this.$('.accept_merge_request').on 'click', -> - $('.automerge_widget.can_be_merged').hide() - $('.merge-in-progress').show() - - this.$('.remove_source_branch').on 'click', -> - $('.remove_source_branch_widget').hide() - $('.remove_source_branch_in_progress').show() - - this.$(".remove_source_branch").on "ajax:success", (e, data, status, xhr) -> - location.reload() - - this.$(".remove_source_branch").on "ajax:error", (e, data, status, xhr) => - this.$('.remove_source_branch_widget').hide() - this.$('.remove_source_branch_in_progress').hide() - this.$('.remove_source_branch_widget.failed').show() - - $('.task-list-item input:checkbox').off('change') - $('.task-list-item input:checkbox').change('merge_request', updateTaskState) - - activateTab: (action) -> - this.$('.merge-request-tabs li').removeClass 'active' - this.$('.tab-content').hide() - switch action - when 'diffs' - this.$('.merge-request-tabs .diffs-tab').addClass 'active' - this.loadDiff() unless @diffs_loaded - this.$('.diffs').show() - $(".diff-header").trigger("sticky_kit:recalc") - when 'commits' - this.$('.merge-request-tabs .commits-tab').addClass 'active' - this.$('.commits').show() - else - this.$('.merge-request-tabs .notes-tab').addClass 'active' - this.$('.notes').show() - - showState: (state) -> - $('.automerge_widget').hide() - $('.automerge_widget.' + state).show() - - showCiState: (state) -> - $('.ci_widget').hide() - allowed_states = ["failed", "canceled", "running", "pending", "success"] - if state in allowed_states - $('.ci_widget.ci-' + state).show() - switch state - when "failed", "canceled" - @setMergeButtonClass('btn-danger') - when "running", "pending" - @setMergeButtonClass('btn-warning') + initTabs: -> + if @opts.action != 'new' + # `MergeRequests#new` has no tab-persisting or lazy-loading behavior + new MergeRequestTabs(@opts) else - $('.ci_widget.ci-error').show() - @setMergeButtonClass('btn-danger') - - showCiCoverage: (coverage) -> - cov_html = $('') - cov_html.addClass('ci-coverage') - cov_html.text('Coverage ' + coverage + '%') - $('.ci_widget:visible').append(cov_html) - - loadDiff: (event) -> - $.ajax - type: 'GET' - url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json" - beforeSend: => - this.$('.mr-loading-status .loading').show() - complete: => - @diffs_loaded = true - this.$('.mr-loading-status .loading').hide() - success: (data) => - this.$(".diffs").html(data.html) - dataType: 'json' + # Show the first tab (Commits) + $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show') showAllCommits: -> this.$('.first-commits').remove() this.$('.all-commits').removeClass 'hide' - alreadyOrCannotBeMerged: -> - this.$('.automerge_widget').hide() - this.$('.merge-in-progress').hide() - this.$('.automerge_widget.already_cannot_be_merged').show() + initTaskList: -> + $('.merge-request-details .js-task-list-container').taskList('enable') + $(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList - setMergeButtonClass: (css_class) -> - $('.accept_merge_request').removeClass("btn-create").addClass(css_class) + disableTaskList: -> + $('.merge-request-details .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container' + + # TODO (rspeicher): Make the merge request description inline-editable like a + # note so that we can re-use its form here + updateTaskList: -> + patchData = {} + patchData['merge_request'] = {'description': $('.js-task-list-field', this).val()} - mergeInProgress: -> $.ajax - type: 'GET' - url: $('.merge-request').data('url') - success: (data) => - switch data.state - when 'merged' - location.reload() - else - setTimeout(merge_request.mergeInProgress, 3000) - dataType: 'json' + type: 'PATCH' + url: $('form.js-issuable-update').attr('action') + data: patchData diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee new file mode 100644 index 0000000000..19a07b6a03 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -0,0 +1,148 @@ +# MergeRequestTabs +# +# Handles persisting and restoring the current tab selection and lazily-loading +# content on the MergeRequests#show page. +# +# ### Example Markup +# +# +# +#
    +#
    +# Notes Content +#
    +#
    +# Commits Content +#
    +#
    +# Diffs Content +#
    +#
    +# +#
    +#
    +# Loading Animation +#
    +#
    +# +class @MergeRequestTabs + diffsLoaded: false + commitsLoaded: false + + constructor: (@opts = {}) -> + # Store the `location` object, allowing for easier stubbing in tests + @_location = location + + @bindEvents() + @activateTab(@opts.action) + + bindEvents: -> + $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown + + tabShown: (event) => + $target = $(event.target) + action = $target.data('action') + + if action == 'commits' + @loadCommits($target.attr('href')) + else if action == 'diffs' + @loadDiff($target.attr('href')) + + @setCurrentAction(action) + + # Activate a tab based on the current action + activateTab: (action) -> + action = 'notes' if action == 'show' + $(".merge-request-tabs a[data-action='#{action}']").tab('show') + + # Replaces the current Merge Request-specific action in the URL with a new one + # + # If the action is "notes", the URL is reset to the standard + # `MergeRequests#show` route. + # + # Examples: + # + # location.pathname # => "/namespace/project/merge_requests/1" + # setCurrentAction('diffs') + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('notes') + # location.pathname # => "/namespace/project/merge_requests/1" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('commits') + # location.pathname # => "/namespace/project/merge_requests/1/commits" + # + # Returns the new URL String + setCurrentAction: (action) => + # Normalize action, just to be safe + action = 'notes' if action == 'show' + + # Remove a trailing '/commits' or '/diffs' + new_state = @_location.pathname.replace(/\/(commits|diffs)(\.html)?\/?$/, '') + + # Append the new action if we're on a tab other than 'notes' + unless action == 'notes' + new_state += "/#{action}" + + # Ensure parameters and hash come along for the ride + new_state += @_location.search + @_location.hash + + # Replace the current history state with the new one without breaking + # Turbolinks' history. + # + # See https://github.com/rails/turbolinks/issues/363 + history.replaceState {turbolinks: true, url: new_state}, document.title, new_state + + new_state + + loadCommits: (source) -> + return if @commitsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.getElementById('commits').innerHTML = data.html + $('.js-timeago').timeago() + @commitsLoaded = true + + loadDiff: (source) -> + return if @diffsLoaded + + @_get + url: "#{source}.json" + @_location.search + success: (data) => + document.getElementById('diffs').innerHTML = data.html + @diffsLoaded = true + + toggleLoading: -> + $('.mr-loading-status .loading').toggle() + + _get: (options) -> + defaults = { + beforeSend: @toggleLoading + complete: @toggleLoading + dataType: 'json' + type: 'GET' + } + + options = $.extend({}, defaults, options) + + $.ajax(options) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee new file mode 100644 index 0000000000..5ab91261d7 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -0,0 +1,56 @@ +class @MergeRequestWidget + # Initialize MergeRequestWidget behavior + # + # check_enable - Boolean, whether to check automerge status + # url_to_automerge_check - String, URL to use to check automerge status + # current_status - String, current automerge status + # ci_enable - Boolean, whether a CI service is enabled + # url_to_ci_check - String, URL to use to check CI status + # + constructor: (@opts) -> + modal = $('#modal_merge_info').modal(show: false) + + mergeInProgress: -> + $.ajax + type: 'GET' + url: $('.merge-request').data('url') + success: (data) => + switch data.state + when 'merged' + location.reload() + else + setTimeout(merge_request_widget.mergeInProgress, 3000) + dataType: 'json' + + getMergeStatus: -> + $.get @opts.url_to_automerge_check, (data) -> + $('.mr-state-widget').replaceWith(data) + + getCiStatus: -> + if @opts.ci_enable + $.get @opts.url_to_ci_check, (data) => + this.showCiState data.status + if data.coverage + this.showCiCoverage data.coverage + , 'json' + + showCiState: (state) -> + $('.ci_widget').hide() + allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] + if state in allowed_states + $('.ci_widget.ci-' + state).show() + switch state + when "failed", "canceled", "not_found" + @setMergeButtonClass('btn-danger') + when "running", "pending" + @setMergeButtonClass('btn-warning') + else + $('.ci_widget.ci-error').show() + @setMergeButtonClass('btn-danger') + + showCiCoverage: (coverage) -> + text = 'Coverage ' + coverage + '%' + $('.ci_widget:visible .ci-coverage').text(text) + + setMergeButtonClass: (css_class) -> + $('.accept_merge_request').removeClass("btn-create").addClass(css_class) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 6dfe10f000..0021d17d85 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,16 +1,24 @@ +#= require autosave +#= require dropzone +#= require dropzone_input +#= require gfm_auto_complete +#= require jquery.atwho +#= require task_list + class @Notes @interval: null - constructor: (notes_url, note_ids, last_fetched_at) -> + constructor: (notes_url, note_ids, last_fetched_at, view) -> @notes_url = notes_url - @notes_url = gon.relative_url_root + @notes_url if gon.relative_url_root? @note_ids = note_ids @last_fetched_at = last_fetched_at + @view = view @noteable_url = document.URL @initRefresh() @setupMainTargetNoteForm() @cleanBinding() @addBinding() + @initTaskList() addBinding: -> # add note to UI after creation @@ -55,13 +63,11 @@ class @Notes # fetch notes when tab becomes visible $(document).on "visibilitychange", @visibilityChange - @notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea' # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown. - $(document).on('keydown', @notes_forms, (e) -> + $(document).on 'keydown', '.js-note-text', (e) -> return if e.originalEvent.repeat if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13) - $(@).parents('form').submit() - ) + $(@).closest('form').submit() cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" @@ -76,11 +82,14 @@ class @Notes $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" - $(document).off "keydown", @notes_forms + $(document).off "keydown", ".js-note-text" $(document).off "keyup", ".js-note-text" $(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-close" + $('.note .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.note .js-task-list-container' + initRefresh: -> clearInterval(Notes.interval) Notes.interval = setInterval => @@ -114,6 +123,7 @@ class @Notes if @isNewNote(note) @note_ids.push(note.id) $('ul.main-notes-list').append(note.html) + @initTaskList() ### Check if note does not exists on page @@ -121,6 +131,8 @@ class @Notes isNewNote: (note) -> $.inArray(note.id, @note_ids) == -1 + isParallelView: -> + @view == 'parallel' ### Render note in discussion area. @@ -268,6 +280,8 @@ class @Notes note_li.replaceWith(note.html) note_li.find('.note-edit-form').hide() note_li.find('.note-body > .note-text').show() + note_li.find('js-task-list-container').taskList('enable') + @enableTaskList() ### Called in response to clicking the edit note link @@ -283,7 +297,7 @@ class @Notes note.find(".note-header").hide() base_form = note.find(".note-edit-form") form = base_form.clone().insertAfter(base_form) - form.addClass('current-note-edit-form') + form.addClass('current-note-edit-form gfm-form') form.find('.div-dropzone').remove() # Show the attachment delete link @@ -296,6 +310,14 @@ class @Notes form.show() textarea = form.find("textarea") textarea.focus() + + # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). + # The textarea has the correct value, Chrome just won't show it unless we + # modify it, so let's clear it and re-set it! + value = textarea.val() + textarea.val "" + textarea.val value + disableButtonIfEmptyField textarea, form.find(".js-comment-button") ### @@ -371,6 +393,7 @@ class @Notes setupDiscussionNoteForm: (dataHolder, form) => # setup note target form.attr "rel", dataHolder.data("discussionId") + form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") form.find("#note_noteable_type").val dataHolder.data("noteableType") @@ -391,19 +414,40 @@ class @Notes form = $(".js-new-note-form") row = $(link).closest("tr") nextRow = row.next() + hasNotes = nextRow.is(".notes_holder") + addForm = false + targetContent = ".notes_content" + rowCssToAdd = "" - # does it already have notes? - if nextRow.is(".notes_holder") - replyButton = nextRow.find(".js-discussion-reply-button") - if replyButton.length > 0 - $.proxy(@replyToDiscussionNote, replyButton).call() + # In parallel view, look inside the correct left/right pane + if @isParallelView() + lineType = $(link).data("lineType") + targetContent += "." + lineType + rowCssToAdd = "" + + if hasNotes + notesContent = nextRow.find(targetContent) + if notesContent.length + replyButton = notesContent.find(".js-discussion-reply-button:visible") + if replyButton.length + e.target = replyButton[0] + $.proxy(@replyToDiscussionNote, replyButton[0], e).call() + else + # In parallel view, the form may not be present in one of the panes + noteForm = notesContent.find(".js-discussion-note-form") + if noteForm.length == 0 + addForm = true else # add a notes row and insert the form - row.after "" - form.clone().appendTo row.next().find(".notes_content") + row.after rowCssToAdd + addForm = true + + if addForm + newForm = form.clone() + newForm.appendTo row.next().find(targetContent) # show the form - @setupDiscussionNoteForm $(link), row.next().find("form") + @setupDiscussionNoteForm $(link), newForm ### Called in response to "cancel" on a diff note form. @@ -479,3 +523,13 @@ class @Notes else form.find('.js-note-target-reopen').text('Reopen') form.find('.js-note-target-close').text('Close') + + initTaskList: -> + @enableTaskList() + $(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList + + enableTaskList: -> + $('.note .js-task-list-container').taskList('enable') + + updateTaskList: -> + $('form', this).submit() diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee index fe83dc0410..d639303aed 100644 --- a/app/assets/javascripts/pager.js.coffee +++ b/app/assets/javascripts/pager.js.coffee @@ -12,7 +12,7 @@ @loading.show() $.ajax type: "GET" - url: location.href + url: $(".content_list").data('href') || location.href data: "limit=" + @limit + "&offset=" + @offset complete: => @loading.hide() diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index de356fbec7..bb0b66b86e 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -1,10 +1,8 @@ class @Profile constructor: -> - $('.edit_user .application-theme input, .edit_user .code-preview-theme input').click -> - # Submit the form - $('.edit_user').submit() - - new Flash("Appearance settings saved", "notice") + # Automatically submit the Preferences form when any of its radio buttons change + $('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> + $(this).parents('form').submit() $('.update-username form').on 'ajax:before', -> $('.loading-gif').show() @@ -12,12 +10,11 @@ class @Profile $(this).find('.update-failed').hide() $('.update-username form').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() + $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() - + $(this).find('.btn-save').enable() $('.js-choose-user-avatar-button').bind "click", -> form = $(this).closest("form") diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index eb8c1fa142..39a433dfc9 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -1,12 +1,12 @@ class @Project constructor: -> # Git clone panel switcher - scope = $ '.git-clone-holder' - if scope.length > 0 - $('a, button', scope).click -> - $('a, button', scope).removeClass 'active' + cloneHolder = $('.git-clone-holder') + if cloneHolder.length + $('a, button', cloneHolder).click -> + $('a, button', cloneHolder).removeClass 'active' $(@).addClass 'active' - $('#project_clone', scope).val $(@).data 'clone' + $('#project_clone', cloneHolder).val $(@).data 'clone' $(".clone").text("").append $(@).data 'clone' # Ref switcher diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee index 836269c44f..fecdb9fc2e 100644 --- a/app/assets/javascripts/project_new.js.coffee +++ b/app/assets/javascripts/project_new.js.coffee @@ -3,9 +3,3 @@ class @ProjectNew $('.project-edit-container').on 'ajax:before', => $('.project-edit-container').hide() $('.save-project-loader').show() - - @initEvents() - - - initEvents: -> - disableButtonIfEmptyField '#project_name', '.project-submit' diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee index 6828ae471e..1fdf28f252 100644 --- a/app/assets/javascripts/project_show.js.coffee +++ b/app/assets/javascripts/project_show.js.coffee @@ -1,15 +1,3 @@ class @ProjectShow constructor: -> - $('.project-home-panel .star').on 'ajax:success', (e, data, status, xhr) -> - $(@).toggleClass('on').find('.count').html(data.star_count) - .on 'ajax:error', (e, xhr, status, error) -> - new Flash('Star toggle failed. Try again later.', 'alert') - - $("a[data-toggle='tab']").on "shown.bs.tab", (e) -> - $.cookie "default_view", $(e.target).attr("href"), { expires: 30, path: '/' } - - defaultView = $.cookie("default_view") - if defaultView - $("a[href=" + defaultView + "]").tab "show" - else - $("a[data-toggle='tab']:first").tab "show" + # I kept class for future diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee new file mode 100644 index 0000000000..bb53219468 --- /dev/null +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -0,0 +1,46 @@ +#= require mousetrap +#= require shortcuts_navigation + +class @ShortcutsIssuable extends ShortcutsNavigation + constructor: (isMergeRequest) -> + super() + Mousetrap.bind('a', -> + $('.js-assignee').select2('open') + return false + ) + Mousetrap.bind('m', -> + $('.js-milestone').select2('open') + return false + ) + Mousetrap.bind('r', => + @replyWithSelectedText() + return false + ) + + if isMergeRequest + @enabledHelp.push('.hidden-shortcut.merge_requests') + else + @enabledHelp.push('.hidden-shortcut.issues') + + replyWithSelectedText: -> + if window.getSelection + selected = window.getSelection().toString() + replyField = $('.js-main-target-form #note_note') + + return if selected.trim() == "" + + # Put a '>' character before each non-empty line in the selection + quote = _.map selected.split("\n"), (val) -> + "> #{val}\n" if val.trim() != '' + + # If replyField already has some content, add a newline before our quote + separator = replyField.val().trim() != "" and "\n" or '' + + replyField.val (_, current) -> + current + separator + quote.join('') + "\n" + + # Trigger autosave for the added text + replyField.trigger('input') + + # Focus the input field + replyField.focus() diff --git a/app/assets/javascripts/shortcuts_issueable.coffee b/app/assets/javascripts/shortcuts_issueable.coffee deleted file mode 100644 index b8dae71e03..0000000000 --- a/app/assets/javascripts/shortcuts_issueable.coffee +++ /dev/null @@ -1,19 +0,0 @@ -#= require shortcuts_navigation - -class @ShortcutsIssueable extends ShortcutsNavigation - constructor: (isMergeRequest) -> - super() - Mousetrap.bind('a', -> - $('.js-assignee').select2('open') - return false - ) - Mousetrap.bind('m', -> - $('.js-milestone').select2('open') - return false - ) - - if isMergeRequest - @enabledHelp.push('.hidden-shortcut.merge_reuests') - else - @enabledHelp.push('.hidden-shortcut.issues') - diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee index 31895fbf2b..5b6f9e7e3f 100644 --- a/app/assets/javascripts/shortcuts_navigation.coffee +++ b/app/assets/javascripts/shortcuts_navigation.coffee @@ -4,6 +4,7 @@ class @ShortcutsNavigation extends Shortcuts constructor: -> super() Mousetrap.bind('g p', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project')) + Mousetrap.bind('g e', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity')) Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree')) Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits')) Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network')) diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 2e3f560825..fb08016fba 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -4,6 +4,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> expanded = 'page-sidebar-expanded' $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") + $('header').toggleClass("header-collapsed header-expanded") $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) ) diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee index 27f0fd31d5..3be14cb43d 100644 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors.js.coffee @@ -1,3 +1,6 @@ +#= require d3 +#= require stat_graph_contributors_util + class @ContributorsStatGraph init: (log) -> @parsed_log = ContributorsStatGraphUtil.parse_log(log) diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee index 8b82d20c6c..b7a0e07376 100644 --- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee @@ -1,3 +1,7 @@ +#= require d3 +#= require jquery +#= require underscore + class @ContributorsGraph MARGIN: top: 20 @@ -46,7 +50,7 @@ class @ContributorsGraph class @ContributorsMasterGraph extends ContributorsGraph constructor: (@data) -> - @width = $('.container').width() - 345 + @width = $('.content').width() - 70 @height = 200 @x = null @y = null @@ -119,7 +123,7 @@ class @ContributorsMasterGraph extends ContributorsGraph class @ContributorsAuthorGraph extends ContributorsGraph constructor: (@data) -> - @width = $('.container').width()/2 - 225 + @width = $('.content').width()/2 - 100 @height = 200 @x = null @y = null diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee index 1670f5c7bc..cfe5508290 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -2,11 +2,15 @@ window.ContributorsStatGraphUtil = parse_log: (log) -> total = {} by_author = {} + by_email = {} for entry in log @add_date(entry.date, total) unless total[entry.date]? - @add_author(entry, by_author) unless by_author[entry.author_name]? - @add_date(entry.date, by_author[entry.author_name]) unless by_author[entry.author_name][entry.date] - @store_data(entry, total[entry.date], by_author[entry.author_name][entry.date]) + + data = by_author[entry.author_name] #|| by_email[entry.author_email] + data ?= @add_author(entry, by_author, by_email) + + @add_date(entry.date, data) unless data[entry.date] + @store_data(entry, total[entry.date], data[entry.date]) total = _.toArray(total) by_author = _.toArray(by_author) total: total, by_author: by_author @@ -15,10 +19,12 @@ window.ContributorsStatGraphUtil = collection[date] = {} collection[date].date = date - add_author: (author, by_author) -> - by_author[author.author_name] = {} - by_author[author.author_name].author_name = author.author_name - by_author[author.author_name].author_email = author.author_email + add_author: (author, by_author, by_email) -> + data = {} + data.author_name = author.author_name + data.author_email = author.author_email + by_author[author.author_name] = data + by_email[author.author_email] = data store_data: (entry, total, by_author) -> @store_commits(total, by_author) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index aeeed9ca3c..9157562a5c 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -6,6 +6,7 @@ class @UsersSelect $('.ajax-users-select').each (i, select) => @projectId = $(select).data('project-id') @groupId = $(select).data('group-id') + @showCurrentUser = $(select).data('current-user') showNullUser = $(select).data('null-user') showAnyUser = $(select).data('any-user') showEmailUser = $(select).data('email-user') @@ -108,6 +109,7 @@ class @UsersSelect active: true project_id: @projectId group_id: @groupId + current_user: @showCurrentUser dataType: "json" ).done (users) -> callback(users) diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee index 66757565d3..81cfc37b95 100644 --- a/app/assets/javascripts/wikis.js.coffee +++ b/app/assets/javascripts/wikis.js.coffee @@ -1,9 +1,17 @@ class @Wikis constructor: -> - $('.build-new-wiki').bind "click", -> + $('.build-new-wiki').bind "click", (e) -> + $('[data-error~=slug]').addClass("hidden") + $('p.hint').show() field = $('#new_wiki_path') - slug = field.val() - path = field.attr('data-wikis-path') + valid_slug_pattern = /^[\w\/-]+$/ - if(slug.length > 0) - location.href = path + "/" + slug + slug = field.val() + if slug.match valid_slug_pattern + path = field.attr('data-wikis-path') + if(slug.length > 0) + location.href = path + "/" + slug + else + e.preventDefault() + $('p.hint').hide() + $('[data-error~=slug]').removeClass("hidden") diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee index 0fb8f7ed75..8a0564a909 100644 --- a/app/assets/javascripts/zen_mode.js.coffee +++ b/app/assets/javascripts/zen_mode.js.coffee @@ -1,6 +1,8 @@ -class @ZenMode - @fullscreen_prefix = 'fullscreen_' +#= require dropzone +#= require mousetrap +#= require mousetrap/pause +class @ZenMode constructor: -> @active_zen_area = null @active_checkbox = null @@ -12,34 +14,31 @@ class @ZenMode $('body').on 'click', '.zen-enter-link', (e) => e.preventDefault() - $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true) + $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true).change() $('body').on 'click', '.zen-leave-link', (e) => e.preventDefault() - $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false) + $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false).change() $('body').on 'change', '.zen-toggle-comment', (e) => checkbox = e.currentTarget if checkbox.checked # Disable other keyboard shortcuts in ZEN mode Mousetrap.pause() - @udpateActiveZenArea(checkbox) + @updateActiveZenArea(checkbox) else @exitZenMode() $(document).on 'keydown', (e) => - if e.keyCode is $.ui.keyCode.ESCAPE + if e.keyCode is 27 # Esc @exitZenMode() e.preventDefault() - $(window).on 'hashchange', @updateZenModeFromLocationHash - - udpateActiveZenArea: (checkbox) => + updateActiveZenArea: (checkbox) => @active_checkbox = $(checkbox) @active_checkbox.prop('checked', true) @active_zen_area = @active_checkbox.parent().find('textarea') @active_zen_area.focus() - window.location.hash = ZenMode.fullscreen_prefix + @active_checkbox.prop('id') exitZenMode: => if @active_zen_area isnt null @@ -47,21 +46,9 @@ class @ZenMode @active_checkbox.prop('checked', false) @active_zen_area = null @active_checkbox = null - window.location.hash = '' - window.scrollTo(window.pageXOffset, @scroll_position) + @restoreScroll(@scroll_position) # Enable dropzone when leaving ZEN mode Dropzone.forElement('.div-dropzone').enable() - checkboxFromLocationHash: (e) -> - id = $.trim(window.location.hash.replace('#' + ZenMode.fullscreen_prefix, '')) - if id - return $('.zennable input[type=checkbox]#' + id)[0] - else - return null - - updateZenModeFromLocationHash: (e) => - checkbox = @checkboxFromLocationHash() - if checkbox - @udpateActiveZenArea(checkbox) - else - @exitZenMode() + restoreScroll: (y) -> + window.scrollTo(window.pageXOffset, y) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 015ff2ce4e..1a5f11df7d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -35,26 +35,26 @@ */ @import "font-awesome"; +/** + * UI themes: + */ +@import "themes/**/*"; + /** * Generic css (forms, nav etc): */ -@import "generic/*"; +@import "generic/**/*"; /** * Page specific styles (issues, projects etc): */ -@import "pages/*"; +@import "pages/**/*"; /** * Code highlight */ -@import "highlight/*"; - -/** - * UI themes: - */ -@import "themes/*"; +@import "highlight/**/*"; /** * Styles for JS behaviors. diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index 62a3eade5c..21acbfa5e5 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -117,7 +117,7 @@ color: #888; text-shadow: 0 1px 1px #fff; } - i[class~="fa"] { + i.fa { line-height: 14px; } } @@ -137,6 +137,12 @@ color: #666; } +.nav-pills > .active > a > span > .badge { + background-color: #fff; + color: $gl-primary; +} + + /** * fix to keep tooltips position in top navigation bar * diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index 62c11b0636..690d89a5c1 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -4,7 +4,7 @@ html { &.touch .tooltip { display: none !important; } body { - padding-top: 46px; + padding-top: $header-height; } } diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index 216f25cdcd..7beef1845e 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -21,6 +21,10 @@ @include border-radius($radius 0 0 $radius) } +@mixin border-radius-right($radius) { + @include border-radius(0 0 $radius $radius) +} + @mixin linear-gradient($from, $to) { background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to)); background-image: -webkit-linear-gradient($from, $to); @@ -50,13 +54,6 @@ @include box-shadow(0 0 0 3px #f1f1f1); } -@mixin header-font { - color: $style_color; - font-size: 16px; - line-height: 44px; - font-weight: normal; -} - @mixin md-typography { font-size: 15px; line-height: 1.5; @@ -73,7 +70,23 @@ font-family: $monospace_font; white-space: pre; word-wrap: normal; - padding: 0; + padding: 1px 2px; + } + + kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #FCFCFC; + border-width: 1px; + border-style: solid; + border-color: #CCC #CCC #BBB; + border-image: none; + border-radius: 3px; + box-shadow: 0px -1px 0px #BBB inset; } h1 { @@ -96,7 +109,7 @@ font-size: 1.2em; } - blockquote p { + blockquote { color: #888; font-size: 15px; line-height: 1.5; @@ -113,7 +126,6 @@ p > code { font-size: inherit; font-weight: inherit; - color: #555; } li { diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 596376c397..cb439a0e0b 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -1,16 +1,20 @@ $style_color: #474D57; -$hover: #FFF3EB; +$hover: #FFFAF1; $gl-text-color: #222222; $gl-link-color: #446e9b; $nprogress-color: #c0392b; $gl-font-size: 14px; $list-font-size: 15px; +$sidebar_collapsed_width: 52px; $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; $border-color: #E5E5E5; $background-color: #f5f5f5; +$header-height: 50px; +$readable-width: 1100px; + /* * State colors: diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 7c3021989a..d36530169a 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -184,7 +184,7 @@ li.note { } } -.supp_diff_link, +.show-suppressed-diff, .show-all-commits { cursor: pointer; } @@ -307,7 +307,7 @@ table { } .btn-sign-in { - margin-top: 5px; + margin-top: 7px; text-shadow: none; } @@ -333,17 +333,8 @@ table { } .search_box { - position: relative; - padding: 30px; + @extend .well; text-align: center; - background-color: #F9F9F9; - border: 1px solid #DDDDDD; - border-radius: 0px; -} - -.search_glyph { - color: #555; - font-size: 42px; } .task-status { @@ -351,9 +342,8 @@ table { } #nprogress .spinner { - top: auto !important; - bottom: 20px !important; - left: 20px !important; + top: 15px !important; + right: 10px !important; } .header-with-avatar { @@ -374,3 +364,12 @@ table { margin-top: 8px; } } + +.profiler-results { + top: 50px !important; + + .profiler-button, + .profiler-controls { + border-color: #EEE !important; + } +} diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index 8014dcb165..f845342c67 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -90,12 +90,7 @@ border-right: none; } background: #fff; - padding: 5px; - } - .author, - .blame_commit { - background: $background-color; - vertical-align: top; + padding: 8px; } .lines { pre { diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 266041403e..4282832e2b 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -49,14 +49,6 @@ label { width: 250px; } -.input-mx-250 { - max-width: 250px; -} - -.input-mn-300 { - min-width: 300px; -} - .custom-form-control { width: 150px; } @@ -89,7 +81,6 @@ label { @include box-shadow(none); } -.issuable-description, .wiki-content { margin-top: 35px; } diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/generic/gfm.scss index 8fac5e534f..bd9200ace2 100644 --- a/app/assets/stylesheets/generic/gfm.scss +++ b/app/assets/stylesheets/generic/gfm.scss @@ -19,3 +19,7 @@ height: 14em; } } + +.gfm-commit, .gfm-commit_range { + font-family: $monospace_font; +} diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss new file mode 100644 index 0000000000..31e2ad8669 --- /dev/null +++ b/app/assets/stylesheets/generic/header.scss @@ -0,0 +1,226 @@ +/* + * Application Header + * + */ +header { + transition-duration: .3s; + + &.navbar-empty { + background: #FFF; + border-bottom: 1px solid #EEE; + + .center-logo { + margin: 8px 0; + text-align: center; + + img { + height: 32px; + } + } + } + + &.navbar-gitlab { + z-index: 100; + margin-bottom: 0; + min-height: $header-height; + border: none; + width: 100%; + + .container { + background: #FFF; + width: 100% !important; + padding: 0; + filter: none; + + .nav > li > a { + color: #888; + font-size: 14px; + padding: 0; + background-color: #f5f5f5; + margin: ($header-height - 28) / 2 0; + margin-left: 10px; + border-radius: 40px; + height: 28px; + width: 28px; + line-height: 28px; + text-align: center; + + &:hover, &:focus, &:active { + background-color: #EEE; + } + } + + .navbar-toggle { + color: #666; + margin: 0; + border-radius: 0; + position: absolute; + right: 2px; + + &:hover { + background-color: #EEE; + } + } + } + } + + .header-logo { + border-bottom: 1px solid transparent; + float: left; + height: $header-height; + width: $sidebar_width; + overflow: hidden; + transition-duration: .3s; + + a { + float: left; + height: $header-height; + width: 100%; + padding: ($header-height - 36 ) / 2 8px; + overflow: hidden; + + img { + width: 36px; + height: 36px; + float: left; + } + + .gitlab-text-container { + width: 230px; + + h3 { + width: 158px; + float: left; + margin: 0; + margin-left: 14px; + font-size: 18px; + line-height: $header-height - 14; + font-weight: normal; + } + } + } + + &:hover { + background-color: #EEE; + } + } + + .header-content { + border-bottom: 1px solid #EEE; + padding-right: 35px; + height: $header-height; + + .title { + margin: 0; + padding: 0 15px 0 35px; + overflow: hidden; + font-size: 18px; + line-height: $header-height; + font-weight: bold; + color: #444; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + + a { + color: #444; + &:hover { + text-decoration: underline; + } + } + } + + .navbar-collapse { + float: right; + } + } + + .search { + margin-right: 10px; + margin-left: 10px; + margin-top: ($header-height - 28) / 2; + + form { + margin: 0; + padding: 0; + } + + .search-input { + width: 220px; + background-image: image-url("icon-search.png"); + background-repeat: no-repeat; + background-position: 10px; + height: inherit; + padding: 4px 6px; + padding-left: 25px; + font-size: 13px; + background-color: #f5f5f5; + border-color: #f5f5f5; + + &:focus { + @include box-shadow(none); + outline: none; + border-color: #DDD; + background-color: #FFF; + } + } + } +} + +@mixin collapsed-header { + .header-logo { + width: $sidebar_collapsed_width; + } + + .header-content { + .title { + margin-left: 30px; + } + } +} + +@media (max-width: $screen-md-max) { + .header-collapsed, .header-expanded { + @include collapsed-header; + } +} + +@media(min-width: $screen-md-max) { + .header-collapsed { + @include collapsed-header; + } + + .header-expanded { + } +} + +@media (max-width: $screen-xs-max) { + header .container { + font-size: 18px; + + .title { + } + + .navbar-nav { + margin: 0px; + float: none !important; + + .visible-xs, .visable-sm { + display: table-cell !important; + } + } + + .navbar-collapse { + padding-left: 5px; + + li { + display: table-cell; + width: 1%; + + a { + margin-left: 8px !important; + } + } + } + } +} diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss index 9558f241b7..869e586839 100644 --- a/app/assets/stylesheets/generic/issue_box.scss +++ b/app/assets/stylesheets/generic/issue_box.scss @@ -6,7 +6,7 @@ .issue-box { display: inline-block; - padding: 7px 13px; + padding: 4px 13px; font-weight: normal; margin-right: 5px; diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss index 08bf6e943d..c502d953c7 100644 --- a/app/assets/stylesheets/generic/lists.scss +++ b/app/assets/stylesheets/generic/lists.scss @@ -39,7 +39,6 @@ &:hover { background: $hover; - border-bottom: 1px solid darken($hover, 10%); } &:last-child { diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index eb39b6bb7e..a4fc82e90b 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -40,6 +40,15 @@ font-size: inherit; } + .div-dropzone-progress { + position: absolute; + top: 7px; + left: -40px; + width: 35px; + font-size: 13px; + text-align: right; + } + .dz-preview { display: none; } @@ -52,6 +61,22 @@ transition: opacity 200ms ease-in-out; } +.md-area { + position: relative; +} + +.md-header ul { + float: left; +} + +.referenced-users { + padding: 10px 0; + color: #999; + margin-left: 10px; + margin-top: 1px; + margin-right: 130px; +} + .md-preview-holder { background: #FFF; border: 1px solid #ddd; diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index 71a1fc4493..bb7b9356c7 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -4,6 +4,11 @@ margin-top: 20px; } + .container-fluid { + padding-left: 5px; + padding-right: 5px; + } + .nav.nav-tabs > li > a { padding: 10px; font-size: 12px; @@ -14,6 +19,10 @@ } } + .referenced-users { + margin-right: 0; + } + .issues-filters, .dash-projects-filters, .check-all-holder { @@ -27,6 +36,50 @@ .project-home-links { display: none; } + + .project-avatar { + display: none; + } + + .project-home-panel { + padding-left: 0 !important; + + .project-avatar { + display: block; + } + + .project-home-desc { + font-size: 21px; + } + + .project-repo-buttons, + .git-clone-holder { + display: none; + } + } + + .project-stats { + display: none; + } + + .container .title { + padding-left: 15px !important; + } + + .issue-info, .merge-request-info { + display: none; + } + + .issue-details { + .creator, + .page-title .btn-close { + display: none; + } + } + + %ul.notes .note-role, .note-actions { + display: none; + } } @media (max-width: $screen-sm-max) { diff --git a/app/assets/stylesheets/generic/nav_sidebar.scss b/app/assets/stylesheets/generic/nav_sidebar.scss deleted file mode 100644 index 3bcb7b8133..0000000000 --- a/app/assets/stylesheets/generic/nav_sidebar.scss +++ /dev/null @@ -1,193 +0,0 @@ -.page-with-sidebar { - background: $background-color; - - .sidebar-wrapper { - position: fixed; - top: 0; - left: 0; - height: 100%; - border-right: 1px solid $border-color; - } -} - -.sidebar-wrapper { - z-index: 99; - background: $background-color; -} - -.content-wrapper { - width: 100%; - padding: 15px; - background: #FFF; -} - -.nav-sidebar { - margin: 0; - list-style: none; - - &.navbar-collapse { - padding: 0px !important; - } -} - -.nav-sidebar li a .count { - float: right; - background: #eee; - padding: 0px 8px; - @include border-radius(6px); -} - -.nav-sidebar li { - &.active a { - color: $text-color; - background: #FFF !important; - font-weight: bold; - border: 1px solid #EEE; - border-right: 1px solid transparent; - border-left: 3px solid $style_color; - - &.no-highlight { - background: none !important; - border: none; - } - - i { - color: $text-color; - } - } -} - -.nav-sidebar li { - &.separate-item { - border-top: 1px solid $border-color; - padding-top: 10px; - margin-top: 10px; - } - - a { - color: $gray; - display: block; - text-decoration: none; - padding: 8px 15px; - font-size: 13px; - line-height: 20px; - padding-left: 20px; - - &:hover { - text-decoration: none; - color: $text-color; - background: $border-color; - } - - &:active, &:focus { - text-decoration: none; - } - - i { - width: 20px; - color: $gray-light; - margin-right: 23px; - } - } -} - -.sidebar-subnav { - margin-left: 0px; - padding-left: 0px; - - li { - list-style: none; - } -} - -@mixin expanded-sidebar { - padding-left: $sidebar_width; - - .sidebar-wrapper { - width: $sidebar_width; - - .nav-sidebar { - margin-top: 29px; - position: fixed; - top: 45px; - width: $sidebar_width; - } - } - - .content-wrapper { - padding: 20px; - } -} - -@mixin folded-sidebar { - padding-left: 50px; - - .sidebar-wrapper { - width: 52px; - - .nav-sidebar { - margin-top: 29px; - position: fixed; - top: 45px; - width: 52px; - - li a { - padding-left: 18px; - font-size: 14px; - padding: 8px 15px; - text-align: center; - - - & > span { - display: none; - } - } - } - - .collapse-nav a { - left: 0px; - padding: 7px 23px 3px 22px; - } - } -} - -.collapse-nav a { - position: fixed; - top: 46px; - padding: 5px 13px 5px 13px; - left: 198px; - font-size: 13px; - background: transparent; - color: black; - border-left: 1px solid $border-color; - border-bottom: 1px solid $border-color; -} - -.collapse-nav a:hover { - text-decoration: none; - background: #f2f6f7; -} - -@media (max-width: $screen-md-max) { - .page-sidebar-collapsed { - @include folded-sidebar; - } - - .page-sidebar-expanded { - @include folded-sidebar; - } - - .collapse-nav { - display: none; - } -} - -@media(min-width: $screen-md-max) { - .page-sidebar-collapsed { - @include folded-sidebar; - } - - .page-sidebar-expanded { - @include expanded-sidebar; - } -} diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss new file mode 100644 index 0000000000..b96664d30d --- /dev/null +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -0,0 +1,190 @@ +.page-with-sidebar { + .sidebar-wrapper { + position: fixed; + top: 0; + bottom: 0; + overflow-y: auto; + overflow-x: hidden; + left: 0; + height: 100%; + transition-duration: .3s; + } +} + +.sidebar-wrapper { + z-index: 99; + background: $background-color; + transition-duration: .3s; +} + +.content-wrapper { + width: 100%; + padding: 20px; + background: #FFF; +} + +.nav-sidebar { + margin-top: 29 + $header-height; + margin-bottom: 50px; + transition-duration: .3s; + list-style: none; + overflow: hidden; + + &.navbar-collapse { + padding: 0px !important; + } + + li { + width: $sidebar_width; + + &.separate-item { + padding-top: 10px; + margin-top: 10px; + } + + a { + padding: 8px 15px; + font-size: 13px; + line-height: 18px; + color: $gray; + display: block; + text-decoration: none; + padding-left: 16px; + + &:hover { + text-decoration: none; + } + + &:active, &:focus { + text-decoration: none; + } + + i { + width: 20px; + color: $gray-light; + margin-right: 23px; + } + + .count { + float: right; + background: #eee; + padding: 0px 8px; + @include border-radius(6px); + } + } + } +} + +.sidebar-subnav { + margin-left: 0px; + padding-left: 0px; + + li { + list-style: none; + } +} + +@mixin expanded-sidebar { + padding-left: $sidebar_width; + transition-duration: .3s; + + .sidebar-wrapper { + width: $sidebar_width; + + .nav-sidebar { + width: $sidebar_width; + } + + .nav-sidebar li a{ + width: 230px; + + &.back-link { + i { + visibility: hidden; + } + } + } + } +} + +@mixin folded-sidebar { + padding-left: 50px; + transition-duration: .3s; + + .sidebar-wrapper { + width: $sidebar_collapsed_width; + + .nav-sidebar { + width: $sidebar_collapsed_width; + + li a { + padding-left: 16px; + } + } + + .collapse-nav a { + left: 0px; + width: $sidebar_collapsed_width; + } + + .sidebar-user { + width: $sidebar_collapsed_width; + } + } +} + +.collapse-nav a { + position: fixed; + top: $header-height; + left: 198px; + font-size: 13px; + background: transparent; + width: 32px; + height: 28px; + text-align: center; + line-height: 28px; + transition-duration: .3s; +} + +.collapse-nav a:hover { + text-decoration: none; + background: #f2f6f7; +} + +@media (max-width: $screen-md-max) { + .page-sidebar-collapsed { + @include folded-sidebar; + } + + .page-sidebar-expanded { + @include folded-sidebar; + } + + .collapse-nav { + display: none; + } +} + +@media(min-width: $screen-md-max) { + .page-sidebar-collapsed { + @include folded-sidebar; + } + + .page-sidebar-expanded { + @include expanded-sidebar; + } +} + +.sidebar-user { + position: fixed; + bottom: 0; + width: $sidebar_width; + padding: 10px; + overflow: hidden; + transition-duration: .3s; + + .username { + margin-top: 5px; + width: $sidebar_width - 2 * 10px; + } +} diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 80190424c1..34b4ee3e17 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -17,12 +17,31 @@ pre { background: #333; color: $background-color; } + + &.plain-readme { + background: none; + border: none; + padding: 0; + margin: 0; + font-size: 14px; + } } .monospace { font-family: $monospace_font; } +code { + &.key-fingerprint { + background: $body-bg; + color: $text-color; + } +} + +a > code { + color: $link-color; +} + /** * Wiki typography * @@ -35,7 +54,14 @@ pre { /* Link to current header. */ h1, h2, h3, h4, h5, h6 { position: relative; - &:hover > :last-child { + + a.anchor { + // Setting `display: none` would prevent the anchor being scrolled to, so + // instead we set the height to 0 and it gets updated on hover. + height: 0; + } + + &:hover > a.anchor { $size: 16px; position: absolute; right: 100%; diff --git a/app/assets/stylesheets/generic/zen.scss b/app/assets/stylesheets/generic/zen.scss index 26afc21a6a..7e86a0fe4b 100644 --- a/app/assets/stylesheets/generic/zen.scss +++ b/app/assets/stylesheets/generic/zen.scss @@ -1,15 +1,14 @@ .zennable { - position: relative; - - input { + .zen-toggle-comment { display: none; } .zen-enter-link { color: #888; position: absolute; - top: -26px; + top: 0px; right: 4px; + line-height: 40px; } .zen-leave-link { @@ -26,10 +25,12 @@ } } + // Hide the Enter link when we're in Zen mode input:checked ~ .zen-backdrop .zen-enter-link { display: none; } + // Show the Leave link when we're in Zen mode input:checked ~ .zen-backdrop .zen-leave-link { display: block; position: absolute; @@ -62,37 +63,24 @@ } } - .zen-backdrop textarea::-webkit-input-placeholder { - color: white; - } - - .zen-backdrop textarea:-moz-placeholder { - color: white; - } - - .zen-backdrop textarea::-moz-placeholder { - color: white; - } - - .zen-backdrop textarea:-ms-input-placeholder { - color: white; - } + // Make the color of the placeholder text in the Zenned-out textarea darker, + // so it becomes visible input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder { - color: #999; + color: #A8A8A8; } input:checked ~ .zen-backdrop textarea:-moz-placeholder { - color: #999; + color: #A8A8A8; opacity: 1; } input:checked ~ .zen-backdrop textarea::-moz-placeholder { - color: #999; + color: #A8A8A8; opacity: 1; } input:checked ~ .zen-backdrop textarea:-ms-input-placeholder { - color: #999; + color: #A8A8A8; } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 84361e1548..359f4073e8 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -29,10 +29,6 @@ .commits-feed-holder { float: right; - - .btn { - padding: 4px 12px; - } } li.commit { diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index af9c83e5dc..9a3b543ad1 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -28,10 +28,6 @@ font-size: 14px; line-height: 24px; - .str-truncated { - max-width: 72%; - } - a { display: block; padding: 8px 15px; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index af6ea58382..1557c243db 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -65,6 +65,17 @@ color: #777; } + .suppressed-container { + padding: ($padding-base-vertical + 5px) $padding-base-horizontal; + text-align: center; + + // "Changes suppressed. Click to show." link + .show-suppressed-diff { + font-size: 110%; + font-weight: bold; + } + } + table { width: 100%; font-family: $monospace_font; diff --git a/app/assets/stylesheets/pages/header.scss b/app/assets/stylesheets/pages/header.scss deleted file mode 100644 index dde19b801f..0000000000 --- a/app/assets/stylesheets/pages/header.scss +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Application Header - * - */ -header { - &.navbar-gitlab { - z-index: 100; - margin-bottom: 0; - min-height: 40px; - border: none; - width: 100%; - - .navbar-inner { - filter: none; - - .nav > li > a { - font-size: 14px; - line-height: 32px; - padding: 6px 10px; - - &:hover, &:focus, &:active { - background: none; - } - } - - /** NAV block with links and profile **/ - .nav { - float: right; - margin-right: 0; - } - - .navbar-toggle { - color: $style_color; - margin: 0; - padding: 10px; - border-radius: 0; - - button i { font-size: 22px; } - - &.collapsed { background-color: transparent !important;} - - &:hover { - background-color: #EEE; - } - } - } - - .turbolink-spinner { - font-size: 20px; - margin-right: 10px; - } - - @media (max-width: $screen-xs-max) { - border-width: 0; - font-size: 18px; - - .title { - @include str-truncated(70%); - } - - .navbar-collapse { - margin-top: 47px; - padding-right: 0; - padding-left: 0; - } - - .navbar-nav { - margin: 5px 0; - - .visible-xs, .visable-sm { - display: table-cell !important; - } - } - - li { - display: table-cell; - width: 1%; - - a { - text-align: center; - font-size: 18px !important; - } - } - } - } - - .container { - width: 100% !important; - padding: 0px; - } - - /** - * - * Logo holder - * - */ - .app_logo { - float: left; - margin-right: 9px; - - a { - float: left; - padding: 5px 0; - height: 46px; - width: 52px; - text-align: center; - - img { - width: 36px; - height: 36px; - } - } - &:hover { - background-color: #EEE; - } - } - - /** - * - * Project / Area name - * - */ - .title { - position: relative; - float: left; - margin: 0; - margin-left: 5px; - @include header-font; - @include str-truncated(37%); - } - - .profile-pic { - padding: 0px !important; - width: 46px; - height: 46px; - margin-left: 5px; - img { - width: 46px; - height: 46px; - } - } - - /** - * - * Search box - * - */ - .search { - margin-right: 10px; - margin-left: 10px; - margin-top: 8px; - - form { - margin: 0; - padding: 0; - } - - .search-input { - background-image: image-url("icon-search.png"); - background-repeat: no-repeat; - background-position: 10px; - height: inherit; - padding: 4px 6px; - padding-left: 25px; - font-size: 13px; - @include border-radius(3px); - border: 1px solid #c6c6c6; - box-shadow: none; - @include transition(all 0.15s ease-in 0s); - } - } -} - -.search .search-input { - width: 300px; - &:focus { - width: 330px; - } -} - -@media (max-width: 1200px) { - .search .search-input { - width: 200px; - &:focus { - width: 230px; - } - } -} - -@media (max-width: $screen-xs-max) { - #nprogress .spinner { - right: 35px !important; - } -} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index a640a4e205..3f617e72b0 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -19,7 +19,7 @@ &.affix { position: fixed; top: 70px; - width: 220px; + margin-right: 35px; } } } @@ -45,3 +45,9 @@ .btn { font-size: 13px; } } + +.issuable-details { + .description { + max-width: $readable-width; + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index cd86a9be8b..3572f33e91 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -25,17 +25,8 @@ display: inline-block; } - .issue-actions { - display: none; - position: absolute; - top: 10px; - right: 15px; - } - - &:hover { - .issue-actions { - display: block; - } + .issue-no-comments { + opacity: 0.5; } } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 8abd4207be..10fce5b3da 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -1,9 +1,15 @@ - - /** - * MR -> show: Automerge widget +/** + * MR -> show: Automerge widget * */ -.automerge_widget { +.mr-state-widget { + background: #FAFAFA; + margin-bottom: 20px; + color: #666; + border: 1px solid #e5e5e5; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); + @include border-radius(3px); + form { margin-bottom: 0; .clearfix { @@ -20,22 +26,71 @@ display: inline-block; margin: 0; margin-left: 20px; - padding: 10px 0; + padding: 5px; line-height: 20px; - font-weight: bold; .remove_source_checkbox { margin: 0; - font-weight: bold; } } } + + .ci_widget { + border-bottom: 1px solid #EEE; + + i { + margin-right: 4px; + } + + &.ci-success { + color: $gl-success; + } + + &.ci-skipped { + background-color: #eee; + color: #888; + } + + &.ci-pending, + &.ci-running { + color: $gl-warning; + } + + &.ci-failed, + &.ci-canceled, + &.ci-error { + color: $gl-danger; + } + } + + .mr-widget-body, + .ci_widget, + .mr-widget-footer { + padding: 15px; + } + + .mr-widget-body { + h4 { + font-weight: bold; + margin: 5px 0; + } + + p:last-child { + margin-bottom: 0; + } + } + + .mr-widget-footer { + border-top: 1px solid #EEE; + } + + .ci-coverage { + float: right; + } } @media(min-width: $screen-sm-max) { .merge-request .merge-request-tabs{ - margin: 20px 0; - li { a { padding: 15px 40px; @@ -45,6 +100,11 @@ } } +.merge-request .merge-request-tabs{ + margin-top: 30px; + margin-bottom: 20px; +} + .mr_source_commit, .mr_target_commit { .commit { @@ -58,23 +118,10 @@ } .label-branch { - @include border-radius(4px); - padding: 3px 4px; - border: none; - background: $hover; - color: #333; + color: #222; font-family: $monospace_font; - font-weight: normal; + font-weight: bold; overflow: hidden; - - .label-project { - @include border-radius-left(4px); - padding: 3px 4px; - background: #279; - position: relative; - left: -4px; - letter-spacing: -1px; - } } .mr-list { @@ -91,11 +138,16 @@ .merge-request-info { color: #999; font-size: 13px; - - .merge-request-labels { - display: inline-block; - } } + + } + + .merge-request-labels { + display: inline-block; + } + + .merge-request-no-comments { + opacity: 0.5; } } @@ -116,65 +168,6 @@ display: none; } -.mr-state-widget { - font-size: 13px; - background: #F9F9F9; - margin-bottom: 20px; - color: #666; - border: 1px solid #EEE; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); - - .ci_widget { - padding: 10px 15px; - font-size: 15px; - border-bottom: 1px solid #BBB; - color: #777; - background-color: $background-color; - - &.ci-success { - color: $gl-success; - border-color: $gl-success; - background-color: #F1FAF1; - } - - &.ci-pending, - &.ci-running { - color: $gl-warning; - border-color: $gl-warning; - background-color: #FAF5F1; - } - - &.ci-failed, - &.ci-canceled, - &.ci-error { - color: $gl-danger; - border-color: $gl-danger; - background-color: #FAF1F1; - } - } - - .mr-widget-body { - padding: 10px 15px; - - h4 { - font-weight: normal; - } - - p:last-child { - margin-bottom: 0; - } - } - - .mr-widget-footer { - padding: 10px 15px; - border-top: 1px solid #EEE; - } - - .ci-coverage { - float: right; - } -} - .merge-request-show-labels { a { margin-right: 5px; @@ -189,3 +182,11 @@ .merge-request-form .select2-container { width: 250px !important; } + +#modal_merge_info .modal-dialog { + width: 600px; +} + +.mr-source-target { + line-height: 31px; +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index a052203078..203f9374ce 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -39,11 +39,8 @@ .new_note, .edit_note { .buttons { - float: left; margin-top: 8px; - } - .clearfix { - margin-bottom: 0; + margin-bottom: 3px; } .note-preview-holder { @@ -82,7 +79,6 @@ .note-form-actions { background: #F9F9F9; - height: 45px; .note-form-option { margin-top: 8px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index facd7e1931..85c828ec1a 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -33,7 +33,16 @@ ul.notes { &:before { content: "\00b7"; } + font-size: 13px; + + a { + @extend .cgray; + + &:hover { + text-decoration: underline; + } + } } .author { color: #333; @@ -44,6 +53,14 @@ ul.notes { } .author-username { } + + .note-role { + float: right; + margin-top: 1px; + border: 1px solid #bbb; + background-color: transparent; + color: #999; + } } .discussion { @@ -55,18 +72,46 @@ ul.notes { .note { display: block; position:relative; + .note-body { overflow: auto; + .note-text { overflow: auto; word-wrap: break-word; @include md-typography; + // Reset ul style types since we're nested inside a ul already + & > ul { + list-style-type: disc; + + ul { + list-style-type: circle; + + ul { + list-style-type: square; + } + } + } + + // Reduce left padding of first task list ul element + ul.task-list:first-child { + padding-left: 10px; + + // sub-tasks should be padded normally + ul { + padding-left: 20px; + } + } + hr { + // Darken 'whitesmoke' a bit to make it more visible in note bodies + border-color: darken(#F5F5F5, 8%); margin: 10px 0; } } } + .note-header { padding-bottom: 3px; } @@ -124,28 +169,23 @@ ul.notes { .discussion, .note { - &.note:hover { - .note-actions { display: block; } - } - .discussion-header:hover { - .discussion-actions { display: block; } - } - .discussion-actions, .note-actions { - display: none; float: right; - - [class~="fa"] { - font-size: 16px; - line-height: 16px; - vertical-align: middle; - } + margin-left: 10px; a { - @extend .cgray; + margin-left: 5px; + + color: #999; + + i.fa { + font-size: 16px; + line-height: 16px; + } &:hover { + @extend .cgray; &.danger { @extend .cred; } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 65655d4bfa..8e4f0eb2b2 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -17,75 +17,15 @@ } } -/* - * Appearance settings - * - */ -.themes_opts { - label { - margin-right: 20px; - text-align: center; - - .prev { - height: 80px; - width: 160px; - margin-bottom: 10px; - @include border-radius(4px); - - &.classic { - background: #31363e; - } - - &.default { - background: #f1f1f1; - } - - &.modern { - background: #009871; - } - - &.gray { - background: #373737; - } - - &.violet { - background: #548; - } - - &.blue { - background: #2980b9; - } - } - } -} - -.code_highlight_opts { - margin-top: 10px; - - label { - margin-right: 20px; - text-align: center; - - .prev { - width: 160px; - margin-bottom: 10px; - - img { - max-width: 100%; - @include border-radius(4px); - } - } - } -} - .oauth-buttons { .btn-group { margin-right: 10px; } .btn { - line-height: 36px; - height: 56px; + line-height: 40px; + height: 42px; + padding: 0px 12px; img { width: 32px; @@ -93,3 +33,17 @@ } } } + +// Profile > Account > Two Factor Authentication +.two-factor-new { + .manual-instructions { + h3 { + margin-top: 0; + } + + // Slightly increase the size of the details so they're easier to read + dl { + font-size: 1.1em; + } + } +} diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss new file mode 100644 index 0000000000..e5859fe738 --- /dev/null +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -0,0 +1,56 @@ +.application-theme { + label { + margin-right: 20px; + text-align: center; + + .preview { + @include border-radius(4px); + + height: 80px; + margin-bottom: 10px; + width: 160px; + + &.ui_blue { + background: $theme-blue; + } + + &.ui_charcoal { + background: $theme-charcoal; + } + + &.ui_graphite { + background: $theme-graphite; + } + + &.ui_gray { + background: $theme-gray; + } + + &.ui_green { + background: $theme-green; + } + + &.ui_violet { + background: $theme-violet; + } + } + } +} + +.syntax-theme { + label { + margin-right: 20px; + text-align: center; + + .preview { + margin-bottom: 10px; + width: 160px; + + img { + @include border-radius(4px); + + max-width: 100%; + } + } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c005470355..29d3dbc25e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -15,59 +15,36 @@ } .project-home-panel { - margin-bottom: 20px; - position: relative; - padding-left: 85px; - - &.empty-project { - border-bottom: 0px; - padding-bottom: 15px; - margin-bottom: 0px; - } + text-align: center; .project-identicon-holder { - position: absolute; - left: 0; + margin-bottom: 15px; - .avatar { - width: 70px; - height: 70px; + .avatar, .identicon { + margin: 0 auto; + float: none; } .identicon { - font-size: 45px; - line-height: 1.6; - } - - .avatar, .identicon { - @include border-radius(4px); - box-shadow: 0 1px 2px #ddd; + @include border-radius(50%); } } - .project-home-dropdown { - margin-left: 10px; - float: right; + .project-home-desc { + h1 { + margin: 0; + margin-bottom: 10px; + font-size: 26px; + } + + p { + display: inline; + } } - .project-home-row { - @extend .clearfix; - margin-bottom: 15px; - - &.project-home-row-top { - margin-bottom: 15px; - } - - .project-home-desc { - font-size: 16px; - line-height: 1.3; - margin-right: 215px; - } - - .project-home-desc { - float: left; - color: $gray; - } + .git-clone-holder { + max-width: 600px; + margin: 20px auto; } .visibility-level-label { @@ -78,36 +55,27 @@ } .project-repo-buttons { - margin-top: -3px; - position: absolute; - right: 0; - width: 260px; - text-align: right; + margin-top: 25px; + margin-bottom: 25px; .btn { + @extend .btn-info; + + margin-left: 10px; font-weight: bold; font-size: 14px; line-height: 16px; + padding: 8px 12px; .count { - padding-left: 10px; - border-left: 1px solid #ccc; + padding-left: 7px; display: inline-block; - margin-left: 10px; + margin-left: 7px; } } } } -.project-home-links { - padding: 10px 0px; - float: right; - a { - margin-left: 10px; - font-weight: 500; - } -} - .git-clone-holder { .project-home-dropdown + & { margin-right: 45px; @@ -122,6 +90,15 @@ .input-group-addon { background: #FAFAFA; + + &.git-protocols { + padding: 0; + border: none; + + .input-group-btn:last-child > .btn { + @include border-radius-right(0); + } + } } } @@ -140,7 +117,7 @@ } .option-descr { - margin-left: 24px; + margin-left: 36px; color: $gray; } } @@ -213,50 +190,41 @@ ul.nav.nav-projects-tabs { } .project-side { - .btn-block { - background-image: none; - - .btn, &.btn { - white-space: normal; - text-align: left; - padding: 10px 15px; - - &.dropdown-toggle { - text-align: center; - } - - &:hover { - background-color: #eee; - border-color: #DDD; - } - } - - .count { - float: right; - font-weight: 500; - text-shadow: 0 1px #FFF; - } - - &.btn-group-justified { - .btn { - width: 100%; - } - .dropdown-toggle { - width: 30px; - padding: 10px; - } - ul { - width: 100%; - } - } - } - .project-fork-icon { float: left; font-size: 26px; margin-right: 10px; line-height: 1.5; } + + .panel { + @include border-radius(3px); + + .panel-heading, .panel-footer { + font-weight: normal; + background-color: transparent; + color: #666; + border-color: #EEE; + } + + .actions { + margin-top: 10px; + } + + .nav-pills a { + padding: 10px; + font-weight: bold; + color: $gl-link-color; + } + + .nav { + margin-bottom: 15px; + } + } + + .ci-status-image { + max-height: 22px; + } } .transfer-project .select2-container { @@ -280,7 +248,8 @@ ul.nav.nav-projects-tabs { } .breadcrumb.repo-breadcrumb { - padding: 2px 0; + padding: 0; + line-height: 34px; background: white; border: none; font-size: 16px; @@ -326,3 +295,24 @@ table.table.protected-branches-list tr.no-border { float: left; margin-right: 10px; } + +.project-stats { + text-align: center; + + ul.nav-pills { display:inline-block; } + li { display:inline; } + a { float:left; } + + li.missing a { + color: #bbb; + border: 1px dashed #ccc; + + &:hover { + background-color: #FAFAFA; + } + } +} + +pre.light-well { + border-color: #f1f1f1; +} diff --git a/app/assets/stylesheets/pages/themes.scss b/app/assets/stylesheets/pages/themes.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 57f63b52aa..5f1a3db4fb 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -89,6 +89,10 @@ td.blame-commit { background: #f9f9f9; min-width: 350px; + + .commit-author-link { + color: #888; + } } td.blame-numbers { pre { @@ -106,20 +110,15 @@ } } -.tree-download-holder .btn { - padding: 4px 12px; -} - .tree-ref-holder { float: left; margin-right: 15px; - - .select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice { - padding: 4px 12px; - } } .readme-holder { + margin: 0 auto; + max-width: $readable-width; + .readme-file-title { font-size: 14px; font-weight: bold; diff --git a/app/assets/stylesheets/themes/dark-theme.scss b/app/assets/stylesheets/themes/dark-theme.scss deleted file mode 100644 index b7b22a8724..0000000000 --- a/app/assets/stylesheets/themes/dark-theme.scss +++ /dev/null @@ -1,63 +0,0 @@ -@mixin dark-theme($color-light, $color, $color-darker, $color-dark) { - header { - &.navbar-gitlab { - .navbar-inner { - background: $color; - - .navbar-toggle { - color: #FFF; - } - - .app_logo, .navbar-toggle { - &:hover { - background-color: $color-darker; - } - } - - .app_logo { - background-color: $color-dark; - } - - .title { - color: #FFF; - - a { - color: #FFF; - &:hover { - text-decoration: underline; - } - } - } - - .search { - .search-input { - background-color: $color-light; - background-color: rgba(255, 255, 255, 0.5); - border: 1px solid $color-light; - - &:focus { - background-color: white; - } - } - } - - .search-input::-webkit-input-placeholder { - color: #666; - } - - .nav > li > a { - color: $color-light; - - &:hover, &:focus, &:active { - background: none; - color: #FFF; - } - } - - .search-input { - border-color: $color-light; - } - } - } - } -} diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/themes/gitlab-theme.scss new file mode 100644 index 0000000000..3589cb88d0 --- /dev/null +++ b/app/assets/stylesheets/themes/gitlab-theme.scss @@ -0,0 +1,120 @@ +/** + * Styles the GitLab application with a specific color theme + * + * $color-light - + * $color - + * $color-darker - + * $color-dark - + */ +@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { + header { + &.navbar-gitlab { + .header-logo { + background-color: $color-darker; + border-color: $color-darker; + + a { + color: $color-light; + } + + &:hover { + background-color: $color-dark; + a { + color: #FFF; + } + } + } + } + } + + .page-with-sidebar { + .collapse-nav a { + color: #FFF; + background: $color; + } + + .sidebar-wrapper { + background: $color-darker; + + .sidebar-user { + background: $color-darker; + color: $color-light; + + &:hover { + background-color: $color-dark; + color: #FFF; + text-decoration: none; + } + } + } + + .nav-sidebar li { + a { + color: $color-light; + + &:hover, &:focus, &:active { + background: $color-dark; + } + + i { + color: $color-light; + } + + .count { + color: $color-light; + background: $color-dark; + } + } + + &.separate-item { + border-top: 1px solid $color; + } + + &.active a { + color: #FFF; + background: $color-dark; + + &.no-highlight { + border: none; + } + + i { + color: #FFF + } + } + } + } +} + +$theme-blue: #2980B9; +$theme-charcoal: #474D57; +$theme-graphite: #888888; +$theme-gray: #373737; +$theme-green: #019875; +$theme-violet: #554488; + +body { + &.ui_blue { + @include gitlab-theme(#BECDE9, $theme-blue, #1970A9, #096099); + } + + &.ui_charcoal { + @include gitlab-theme(#979DA7, $theme-charcoal, #373D47, #24272D); + } + + &.ui_graphite { + @include gitlab-theme(#CCCCCC, $theme-graphite, #777777, #666666); + } + + &.ui_gray { + @include gitlab-theme(#979797, $theme-gray, #272727, #222222); + } + + &.ui_green { + @include gitlab-theme(#AADDCC, $theme-green, #018865, #017855); + } + + &.ui_violet { + @include gitlab-theme(#9988CC, $theme-violet, #443366, #332255); + } +} diff --git a/app/assets/stylesheets/themes/ui_basic.scss b/app/assets/stylesheets/themes/ui_basic.scss deleted file mode 100644 index 097d5c5b73..0000000000 --- a/app/assets/stylesheets/themes/ui_basic.scss +++ /dev/null @@ -1,30 +0,0 @@ -/** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - */ -.ui_basic { - header { - &.navbar-gitlab { - .navbar-inner { - background: #F1F1F1; - border-bottom: 1px solid #DDD; - - .title { - color: #555; - - a { - color: #555; - &:hover { - text-decoration: underline; - } - } - } - - .nav > li > a { - color: $style_color; - } - } - } - } -} diff --git a/app/assets/stylesheets/themes/ui_blue.scss b/app/assets/stylesheets/themes/ui_blue.scss deleted file mode 100644 index e223058be8..0000000000 --- a/app/assets/stylesheets/themes/ui_blue.scss +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Blue GitLab UI theme - */ -.ui_blue { - @include dark-theme(#BECDE9, #2980b9, #1970a9, #096099); -} diff --git a/app/assets/stylesheets/themes/ui_color.scss b/app/assets/stylesheets/themes/ui_color.scss deleted file mode 100644 index 7ac6903b2e..0000000000 --- a/app/assets/stylesheets/themes/ui_color.scss +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Violet GitLab UI theme - */ -.ui_color { - @include dark-theme(#98C, #548, #436, #325); -} diff --git a/app/assets/stylesheets/themes/ui_gray.scss b/app/assets/stylesheets/themes/ui_gray.scss deleted file mode 100644 index 9257e5f4d4..0000000000 --- a/app/assets/stylesheets/themes/ui_gray.scss +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Gray GitLab UI theme - */ -.ui_gray { - @include dark-theme(#979797, #373737, #272727, #222222); -} diff --git a/app/assets/stylesheets/themes/ui_mars.scss b/app/assets/stylesheets/themes/ui_mars.scss deleted file mode 100644 index 4caf5843d9..0000000000 --- a/app/assets/stylesheets/themes/ui_mars.scss +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Classic GitLab UI theme - */ -.ui_mars { - @include dark-theme(#979DA7, #474D57, #373D47, #24272D); -} diff --git a/app/assets/stylesheets/themes/ui_modern.scss b/app/assets/stylesheets/themes/ui_modern.scss deleted file mode 100644 index 7044988231..0000000000 --- a/app/assets/stylesheets/themes/ui_modern.scss +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Modern GitLab UI theme - */ -.ui_modern { - @include dark-theme(#ADC, #019875, #018865, #017855); -} diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb new file mode 100644 index 0000000000..65dbd5ef55 --- /dev/null +++ b/app/controllers/abuse_reports_controller.rb @@ -0,0 +1,24 @@ +class AbuseReportsController < ApplicationController + def new + @abuse_report = AbuseReport.new + @abuse_report.user_id = params[:user_id] + end + + def create + @abuse_report = AbuseReport.new(report_params) + @abuse_report.reporter = current_user + + if @abuse_report.save + message = "Thank you for your report. A GitLab administrator will look into it shortly." + redirect_to root_path, notice: message + else + render :new + end + end + + private + + def report_params + params.require(:abuse_report).permit(:user_id, :message) + end +end diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb new file mode 100644 index 0000000000..38a5a9fca0 --- /dev/null +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -0,0 +1,16 @@ +class Admin::AbuseReportsController < Admin::ApplicationController + def index + @abuse_reports = AbuseReport.order(id: :desc).page(params[:page]) + end + + def destroy + abuse_report = AbuseReport.find(params[:id]) + + if params[:remove_user] + abuse_report.user.destroy + end + + abuse_report.destroy + render nothing: true + end +end diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 6a8f20f604..56e2438646 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -2,8 +2,8 @@ # # Automatically sets the layout and ensures an administrator is logged in class Admin::ApplicationController < ApplicationController + before_action :authenticate_admin! layout 'admin' - before_filter :authenticate_admin! def authenticate_admin! return render_404 unless current_user.is_admin? diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index b5fda196bf..c7c643db40 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -1,5 +1,5 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController - before_filter :set_application_setting + before_action :set_application_setting def show end @@ -38,8 +38,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :twitter_sharing_enabled, :sign_in_text, :home_page_url, + :after_sign_out_path, :max_attachment_size, - restricted_visibility_levels: [] + :session_expire_delay, + :default_project_visibility, + :default_snippet_visibility, + :restricted_signup_domains_raw, + :version_check_enabled, + :user_oauth_applications, + restricted_visibility_levels: [], ) end end diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index e1643bb34b..0808024fc3 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -1,5 +1,5 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController - before_filter :broadcast_messages + before_action :broadcast_messages def index @broadcast_message = BroadcastMessage.new diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index e93603bef3..285e849534 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -1,13 +1,8 @@ class Admin::DeployKeysController < Admin::ApplicationController - before_filter :deploy_keys, only: [:index] - before_filter :deploy_key, only: [:show, :destroy] + before_action :deploy_keys, only: [:index] + before_action :deploy_key, only: [:destroy] def index - - end - - def show - end def new diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 22d045fc38..4d3e48f7f8 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,5 +1,5 @@ class Admin::GroupsController < Admin::ApplicationController - before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update] + before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update] def index @groups = Group.all @@ -47,7 +47,7 @@ class Admin::GroupsController < Admin::ApplicationController end def destroy - @group.destroy + DestroyGroupService.new(@group, current_user).execute redirect_to admin_groups_path, notice: 'Group was successfully deleted.' end diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 0a463239d7..690096bdbc 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -33,7 +33,7 @@ class Admin::HooksController < Admin::ApplicationController owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data) + @hook.execute(data, 'system_hooks') redirect_to :back end diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb new file mode 100644 index 0000000000..d28614731f --- /dev/null +++ b/app/controllers/admin/identities_controller.rb @@ -0,0 +1,41 @@ +class Admin::IdentitiesController < Admin::ApplicationController + before_action :user + before_action :identity, except: :index + + def index + @identities = @user.identities + end + + def edit + end + + def update + if @identity.update_attributes(identity_params) + redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.' + else + render :edit + end + end + + def destroy + if @identity.destroy + redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.' + else + redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.' + end + end + + protected + + def user + @user ||= User.find_by!(username: params[:user_id]) + end + + def identity + @identity ||= user.identities.find(params[:id]) + end + + def identity_params + params.require(:identity).permit(:provider, :extern_uid) + end +end diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index 21111bb44f..cb33fdd976 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -1,5 +1,5 @@ class Admin::KeysController < Admin::ApplicationController - before_filter :user, only: [:show, :destroy] + before_action :user, only: [:show, :destroy] def show @key = user.keys.find(params[:id]) diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 5176a8399a..da5f5bb83f 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,11 +1,11 @@ class Admin::ProjectsController < Admin::ApplicationController - before_filter :project, only: [:show, :transfer] - before_filter :group, only: [:show, :transfer] - before_filter :repository, only: [:show, :transfer] + before_action :project, only: [:show, :transfer] + before_action :group, only: [:show, :transfer] + before_action :repository, only: [:show, :transfer] def index @projects = Project.all - @projects = @projects.where(namespace_id: params[:namespace_id]) if params[:namespace_id].present? + @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @@ -23,7 +23,8 @@ class Admin::ProjectsController < Admin::ApplicationController end def transfer - ::Projects::TransferService.new(@project, current_user, params.dup).execute + namespace = Namespace.find_by(id: params[:new_namespace_id]) + ::Projects::TransferService.new(@project, current_user, params.dup).execute(namespace) @project.reload redirect_to admin_namespace_project_path(@project.namespace, @project) diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 76a938c5fe..a62170662e 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -1,5 +1,5 @@ class Admin::ServicesController < Admin::ApplicationController - before_filter :service, only: [:edit, :update] + before_action :service, only: [:edit, :update] def index @services = services_templates @@ -40,15 +40,6 @@ class Admin::ServicesController < Admin::ApplicationController def application_services_params params.permit(:id, - service: [ - :title, :token, :type, :active, :api_key, :subdomain, - :room, :recipients, :project_url, :webhook, - :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, - :build_key, :server, :teamcity_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch, - :send_from_committer_email, :disable_diffs, - :push_events, :tag_push_events, :note_events, :issues_events, - :merge_requests_events - ]) + service: Projects::ServicesController::ALLOWED_PARAMS) end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index b4c011f213..6092c79c25 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,5 +1,5 @@ class Admin::UsersController < Admin::ApplicationController - before_filter :user, only: [:show, :edit, :update, :destroy] + before_action :user, except: [:index, :new, :create] def index @users = User.order_name_asc.filter(params[:filter]) @@ -9,8 +9,17 @@ class Admin::UsersController < Admin::ApplicationController end def show + end + + def projects @personal_projects = user.personal_projects @joined_projects = user.projects.joined(@user) + end + + def groups + end + + def keys @keys = user.keys end @@ -38,6 +47,28 @@ class Admin::UsersController < Admin::ApplicationController end end + def unlock + if user.unlock_access! + redirect_to :back, alert: "Successfully unlocked" + else + redirect_to :back, alert: "Error occurred. User was not unlocked" + end + end + + def confirm + if user.confirm! + redirect_to :back, notice: "Successfully confirmed" + else + redirect_to :back, alert: "Error occurred. User was not confirmed" + end + end + + def disable_two_factor + user.disable_two_factor! + redirect_to admin_user_path(user), + notice: 'Two-factor Authentication has been disabled for this user' + end + def create opts = { force_random_password: true, @@ -86,11 +117,7 @@ class Admin::UsersController < Admin::ApplicationController end def destroy - # 1. Remove groups where user is the only owner - user.solo_owned_groups.map(&:destroy) - - # 2. Remove user with all authored content including personal projects - user.destroy + DeleteUserService.new(current_user).execute(user) respond_to do |format| format.html { redirect_to admin_users_path } @@ -102,8 +129,7 @@ class Admin::UsersController < Admin::ApplicationController email = user.emails.find(params[:email_id]) email.destroy - user.set_notification_email - user.save if user.notification_email_changed? + user.update_secondary_emails! respond_to do |format| format.html { redirect_to :back, notice: "Successfully removed email." } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 920a981e7c..3ce8dbc940 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,18 +3,19 @@ require 'gon' class ApplicationController < ActionController::Base include Gitlab::CurrentSettings include GitlabRoutingHelper + include PageLayoutHelper PER_PAGE = 20 - before_filter :authenticate_user_from_token! - before_filter :authenticate_user! - before_filter :reject_blocked! - before_filter :check_password_expiration - before_filter :ldap_security_check - before_filter :default_headers - before_filter :add_gon_variables - before_filter :configure_permitted_parameters, if: :devise_controller? - before_filter :require_email, unless: :devise_controller? + before_action :authenticate_user_from_token! + before_action :authenticate_user! + before_action :reject_blocked! + before_action :check_password_expiration + before_action :ldap_security_check + before_action :default_headers + before_action :add_gon_variables + before_action :configure_permitted_parameters, if: :devise_controller? + before_action :require_email, unless: :devise_controller? protect_from_forgery with: :exception @@ -55,7 +56,7 @@ class ApplicationController < ActionController::Base def authenticate_user!(*args) # If user is not signed-in and tries to access root_path - redirect him to landing page if current_application_settings.home_page_url.present? - if current_user.nil? && controller_name == 'dashboard' && action_name == 'show' + if current_user.nil? && root_path == request.path redirect_to current_application_settings.home_page_url and return end end @@ -87,6 +88,10 @@ class ApplicationController < ActionController::Base end end + def after_sign_out_path_for(resource) + current_application_settings.after_sign_out_path || new_user_session_path + end + def abilities Ability.abilities end @@ -135,11 +140,6 @@ class ApplicationController < ActionController::Base return access_denied! unless can?(current_user, action, project) end - def authorize_labels! - # Labels should be accessible for issues and/or merge requests - authorize_read_issue! || authorize_read_merge_request! - end - def access_denied! render "errors/access_denied", layout: "errors", status: 404 end @@ -178,24 +178,15 @@ class ApplicationController < ActionController::Base response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end - def default_url_options - if !Rails.env.test? - port = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? - { host: Gitlab.config.gitlab.host, - protocol: Gitlab.config.gitlab.protocol, - port: port, - script_name: Gitlab.config.gitlab.relative_url_root } - else - super - end - end - def default_headers headers['X-Frame-Options'] = 'DENY' headers['X-XSS-Protection'] = '1; mode=block' headers['X-UA-Compatible'] = 'IE=edge' headers['X-Content-Type-Options'] = 'nosniff' - headers['Strict-Transport-Security'] = 'max-age=31536000' if Gitlab.config.gitlab.https + # Enabling HSTS for non-standard ports would send clients to the wrong port + if Gitlab.config.gitlab.https and Gitlab.config.gitlab.port == 443 + headers['Strict-Transport-Security'] = 'max-age=31536000' + end end def add_gon_variables @@ -259,7 +250,7 @@ class ApplicationController < ActionController::Base end def configure_permitted_parameters - devise_parameter_sanitizer.sanitize(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me) } + devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) } end def hexdigest(string) @@ -277,6 +268,7 @@ class ApplicationController < ActionController::Base params[:scope] = 'all' if params[:scope].blank? params[:state] = 'opened' if params[:state].blank? + @sort = params[:sort] @filter_params = params.dup if @project @@ -294,52 +286,27 @@ class ApplicationController < ActionController::Base @filter_params end - def set_filter_values(collection) - assignee_id = @filter_params[:assignee_id] - author_id = @filter_params[:author_id] - milestone_id = @filter_params[:milestone_id] - - @sort = @filter_params[:sort] - @assignees = User.where(id: collection.pluck(:assignee_id)) - @authors = User.where(id: collection.pluck(:author_id)) - @milestones = Milestone.where(id: collection.pluck(:milestone_id)) - - if assignee_id.present? && !assignee_id.to_i.zero? - @assignee = @assignees.find_by(id: assignee_id) - end - - if author_id.present? && !author_id.to_i.zero? - @author = @authors.find_by(id: author_id) - end - - if milestone_id.present? && !milestone_id.to_i.zero? - @milestone = @milestones.find_by(id: milestone_id) - end - end - def get_issues_collection set_filters_params - issues = IssuesFinder.new.execute(current_user, @filter_params) - set_filter_values(issues) - issues + @issuable_finder = IssuesFinder.new(current_user, @filter_params) + @issuable_finder.execute end def get_merge_requests_collection set_filters_params - merge_requests = MergeRequestsFinder.new.execute(current_user, @filter_params) - set_filter_values(merge_requests) - merge_requests + @issuable_finder = MergeRequestsFinder.new(current_user, @filter_params) + @issuable_finder.execute end def github_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:github) + Gitlab::OAuth::Provider.enabled?(:github) end def gitlab_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:gitlab) + Gitlab::OAuth::Provider.enabled?(:gitlab) end def bitbucket_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? + Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 11af989526..904d26a39f 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,25 +1,46 @@ class AutocompleteController < ApplicationController + skip_before_action :authenticate_user!, only: [:users] + def users - @users = - if params[:project_id].present? - project = Project.find(params[:project_id]) + begin + @users = + if params[:project_id].present? + project = Project.find(params[:project_id]) - if can?(current_user, :read_project, project) - project.team.users - end - elsif params[:group_id] - group = Group.find(params[:group_id]) + if can?(current_user, :read_project, project) + project.team.users + end + elsif params[:group_id] + group = Group.find(params[:group_id]) - if can?(current_user, :read_group, group) - group.users + if can?(current_user, :read_group, group) + group.users + end + elsif current_user + User.all end - else - User.all + rescue ActiveRecord::RecordNotFound + if current_user + return render json: {}, status: 404 end + end + if @users.nil? && current_user.nil? + authenticate_user! + end + + @users ||= User.none @users = @users.search(params[:search]) if params[:search].present? @users = @users.active @users = @users.page(params[:page]).per(PER_PAGE) + + unless params[:search].present? + # Include current user if available to filter by "Me" + if params[:current_user] && current_user + @users = [*@users, current_user].uniq + end + end + render json: @users, only: [:name, :username, :id], methods: [:avatar_url] end diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb new file mode 100644 index 0000000000..d5918a7af3 --- /dev/null +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -0,0 +1,30 @@ +# == AuthenticatesWithTwoFactor +# +# Controller concern to handle two-factor authentication +# +# Upon inclusion, skips `require_no_authentication` on `:create`. +module AuthenticatesWithTwoFactor + extend ActiveSupport::Concern + + included do + # This action comes from DeviseController, but because we call `sign_in` + # manually, not skipping this action would cause a "You are already signed + # in." error message to be shown upon successful login. + skip_before_action :require_no_authentication, only: [:create] + end + + # Store the user's ID in the session for later retrieval and render the + # two factor code prompt + # + # The user must have been authenticated with a valid login and password + # before calling this method! + # + # user - User record + # + # Returns nil + def prompt_for_two_factor(user) + session[:otp_user_id] = user.id + + render 'devise/sessions/two_factor' and return + end +end diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb new file mode 100644 index 0000000000..962ea38d6c --- /dev/null +++ b/app/controllers/dashboard/application_controller.rb @@ -0,0 +1,3 @@ +class Dashboard::ApplicationController < ApplicationController + layout 'dashboard' +end diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index ed14f4e1f3..3bc94ff218 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,4 +1,4 @@ -class Dashboard::GroupsController < ApplicationController +class Dashboard::GroupsController < Dashboard::ApplicationController def index @group_members = current_user.group_members.page(params[:page]).per(PER_PAGE) end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index cb51792df1..53896d4f2c 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -1,5 +1,5 @@ -class Dashboard::MilestonesController < ApplicationController - before_filter :load_projects +class Dashboard::MilestonesController < Dashboard::ApplicationController + before_action :load_projects def index project_milestones = case params[:state] diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 56e6fcc41c..da96171e88 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -1,5 +1,5 @@ -class Dashboard::ProjectsController < ApplicationController - before_filter :event_filter +class Dashboard::ProjectsController < Dashboard::ApplicationController + before_action :event_filter def starred @projects = current_user.starred_projects diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 9bd853ed5c..d2f0c43929 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,8 +1,8 @@ -class DashboardController < ApplicationController - respond_to :html +class DashboardController < Dashboard::ApplicationController + before_action :load_projects + before_action :event_filter, only: :show - before_filter :load_projects, except: [:projects] - before_filter :event_filter, only: :show + respond_to :html def show @projects = @projects.includes(:namespace) diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb new file mode 100644 index 0000000000..4b275033d2 --- /dev/null +++ b/app/controllers/explore/application_controller.rb @@ -0,0 +1,3 @@ +class Explore::ApplicationController < ApplicationController + layout 'explore' +end diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index c51a4a211a..55cda0cff1 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,9 +1,7 @@ -class Explore::GroupsController < ApplicationController - skip_before_filter :authenticate_user!, +class Explore::GroupsController < Explore::ApplicationController + skip_before_action :authenticate_user!, :reject_blocked, :set_current_user_for_observers - layout "explore" - def index @groups = GroupsFinder.new.execute(current_user) @groups = @groups.search(params[:search]) if params[:search].present? diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index b295f295bb..e9bcb44f6b 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,9 +1,7 @@ -class Explore::ProjectsController < ApplicationController - skip_before_filter :authenticate_user!, +class Explore::ProjectsController < Explore::ApplicationController + skip_before_action :authenticate_user!, :reject_blocked - layout 'explore' - def index @projects = ProjectsFinder.new.execute(current_user) @tags = @projects.tags_on(:tags) diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 469a6813ee..6878d4bc07 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -1,4 +1,5 @@ class Groups::ApplicationController < ApplicationController + layout 'group' private @@ -17,12 +18,10 @@ class Groups::ApplicationController < ApplicationController return render_404 end end - - def determine_layout - if current_user - 'group' - else - 'public_group' + + def authorize_admin_group_member! + unless can?(current_user, :admin_group_member, group) + return render_403 end end end diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb index 38071410f4..6aa64222f7 100644 --- a/app/controllers/groups/avatars_controller.rb +++ b/app/controllers/groups/avatars_controller.rb @@ -1,6 +1,4 @@ class Groups::AvatarsController < ApplicationController - layout "profile" - def destroy @group = Group.find_by(path: params[:group_id]) @group.remove_avatar! diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 265cf4f0f4..91518c44a9 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,12 +1,11 @@ class Groups::GroupMembersController < Groups::ApplicationController - skip_before_filter :authenticate_user!, only: [:index] - before_filter :group + skip_before_action :authenticate_user!, only: [:index] + before_action :group # Authorize - before_filter :authorize_read_group! - before_filter :authorize_admin_group!, except: [:index, :leave] - - layout :determine_layout + before_action :authorize_read_group! + before_action :authorize_admin_group!, except: [:index, :leave] + before_action :authorize_admin_group_member!, only: [:create, :resend_invite] def index @project = @group.projects.find(params[:project_id]) if params[:project_id] @@ -30,6 +29,9 @@ class Groups::GroupMembersController < Groups::ApplicationController def update @member = @group.group_members.find(params[:id]) + + return render_403 unless can?(current_user, :update_group_member, @member) + @member.update_attributes(member_params) end @@ -49,7 +51,7 @@ class Groups::GroupMembersController < Groups::ApplicationController def resend_invite redirect_path = group_group_members_path(@group) - + @group_member = @group.group_members.find(params[:id]) if @group_member.invite? @@ -63,12 +65,16 @@ class Groups::GroupMembersController < Groups::ApplicationController def leave @group_member = @group.group_members.where(user_id: current_user.id).first - + if can?(current_user, :destroy_group_member, @group_member) @group_member.destroy redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") else - return render_403 + if @group.last_owner?(current_user) + redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.") + else + return render_403 + end end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 546ff2cc71..669f7f3126 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,7 +1,5 @@ -class Groups::MilestonesController < ApplicationController - layout 'group' - - before_filter :authorize_group_milestone!, only: :update +class Groups::MilestonesController < Groups::ApplicationController + before_action :authorize_group_milestone!, only: :update def index project_milestones = case params[:state] diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7af3c07718..279c6ef0f4 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,17 +1,16 @@ class GroupsController < Groups::ApplicationController - skip_before_filter :authenticate_user!, only: [:show, :issues, :merge_requests] + skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests] respond_to :html - before_filter :group, except: [:new, :create] + before_action :group, except: [:new, :create] # Authorize - before_filter :authorize_read_group!, except: [:new, :create] - before_filter :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] - before_filter :authorize_create_group!, only: [:new, :create] + before_action :authorize_read_group!, except: [:new, :create] + before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] + before_action :authorize_create_group!, only: [:new, :create] # Load group projects - before_filter :load_projects, except: [:new, :create, :projects, :edit, :update] - before_filter :event_filter, only: :show - before_filter :set_title, only: [:new, :create] + before_action :load_projects, except: [:new, :create, :projects, :edit, :update] + before_action :event_filter, only: :show layout :determine_layout @@ -25,7 +24,7 @@ class GroupsController < Groups::ApplicationController if @group.save @group.add_owner(current_user) - redirect_to @group, notice: 'Group was successfully created.' + redirect_to @group, notice: "Group '#{@group.name}' was successfully created." else render action: "new" end @@ -76,16 +75,16 @@ class GroupsController < Groups::ApplicationController def update if @group.update_attributes(group_params) - redirect_to edit_group_path(@group), notice: 'Group was successfully updated.' + redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else render action: "edit" end end def destroy - @group.destroy + DestroyGroupService.new(@group, current_user).execute - redirect_to root_path, notice: 'Group was removed.' + redirect_to root_path, alert: "Group '#{@group.name} was deleted." end protected @@ -119,17 +118,13 @@ class GroupsController < Groups::ApplicationController end end - def set_title - @title = 'New Group' - end - def determine_layout if [:new, :create].include?(action_name.to_sym) - 'navless' - elsif current_user - 'group' + 'application' + elsif [:edit, :update, :projects].include?(action_name.to_sym) + 'group_settings' else - 'public_group' + 'group' end end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 35ece5b270..71831c5380 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,14 +1,17 @@ class HelpController < ApplicationController + layout 'help' + def index end def show - category = clean_path_info(path_params[:category]) - file = path_params[:file] + @category = clean_path_info(path_params[:category]) + @file = path_params[:file] respond_to do |format| format.any(:markdown, :md, :html) do - path = Rails.root.join('doc', category, "#{file}.md") + # Note: We are purposefully NOT using `Rails.root.join` + path = File.join(Rails.root, 'doc', @category, "#{@file}.md") if File.exist?(path) @markdown = File.read(path) @@ -22,7 +25,8 @@ class HelpController < ApplicationController # Allow access to images in the doc folder format.any(:png, :gif, :jpeg) do - path = Rails.root.join('doc', category, "#{file}.#{params[:format]}") + # Note: We are purposefully NOT using `Rails.root.join` + path = File.join(Rails.root, 'doc', @category, "#{@file}.#{params[:format]}") if File.exist?(path) send_file(path, disposition: 'inline') diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index bb8d7e0235..4e6c0b6663 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -1,15 +1,16 @@ class Import::BitbucketController < Import::BaseController - before_filter :verify_bitbucket_import_enabled - before_filter :bitbucket_auth, except: :callback + before_action :verify_bitbucket_import_enabled + before_action :bitbucket_auth, except: :callback rescue_from OAuth::Error, with: :bitbucket_unauthorized + rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized def callback - request_token = session.delete(:oauth_request_token) + request_token = session.delete(:oauth_request_token) raise "Session expired!" if request_token.nil? request_token.symbolize_keys! - + access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url) current_user.bitbucket_access_token = access_token.token @@ -21,7 +22,8 @@ class Import::BitbucketController < Import::BaseController def status @repos = client.projects - + @incompatible_repos = client.incompatible_projects + @already_added_projects = current_user.created_projects.where(import_type: "bitbucket") already_added_projects_names = @already_added_projects.pluck(:import_source) @@ -41,7 +43,7 @@ class Import::BitbucketController < Import::BaseController repo_owner = repo["owner"] repo_owner = current_user.username if repo_owner == client.user["user"]["username"] @target_namespace = params[:new_namespace].presence || repo_owner - + namespace = get_or_create_namespace || (render and return) unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user).execute diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 87b41454c7..b9f99c1b88 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,6 +1,6 @@ class Import::GithubController < Import::BaseController - before_filter :verify_github_import_enabled - before_filter :github_auth, except: :callback + before_action :verify_github_import_enabled + before_action :github_auth, except: :callback rescue_from Octokit::Unauthorized, with: :github_unauthorized @@ -36,7 +36,7 @@ class Import::GithubController < Import::BaseController repo_owner = repo.owner.login repo_owner = current_user.username if repo_owner == client.user.login @target_namespace = params[:new_namespace].presence || repo_owner - + namespace = get_or_create_namespace || (render and return) @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user).execute diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index bddbfded81..1b8962d892 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -1,6 +1,6 @@ class Import::GitlabController < Import::BaseController - before_filter :verify_gitlab_import_enabled - before_filter :gitlab_auth, except: :callback + before_action :verify_gitlab_import_enabled + before_action :gitlab_auth, except: :callback rescue_from OAuth2::Error, with: :gitlab_unauthorized @@ -13,7 +13,7 @@ class Import::GitlabController < Import::BaseController def status @repos = client.projects - + @already_added_projects = current_user.created_projects.where(import_type: "gitlab") already_added_projects_names = @already_added_projects.pluck(:import_source) @@ -33,7 +33,7 @@ class Import::GitlabController < Import::BaseController repo_owner = repo["namespace"]["path"] repo_owner = current_user.username if repo_owner == client.user["username"] @target_namespace = params[:new_namespace].presence || repo_owner - + namespace = get_or_create_namespace || (render and return) @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user).execute diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb index 6067a87ee0..c121d2de7c 100644 --- a/app/controllers/import/gitorious_controller.rb +++ b/app/controllers/import/gitorious_controller.rb @@ -6,7 +6,7 @@ class Import::GitoriousController < Import::BaseController def callback session[:gitorious_repos] = params[:repos] - redirect_to status_import_gitorious_url + redirect_to status_import_gitorious_path end def status diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 6574be9192..4aa6d28c9a 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -1,8 +1,8 @@ class Import::GoogleCodeController < Import::BaseController - before_filter :user_map, only: [:new_user_map, :create_user_map] + before_action :user_map, only: [:new_user_map, :create_user_map] def new - + end def callback @@ -68,10 +68,11 @@ class Import::GoogleCodeController < Import::BaseController def status unless client.valid? - return redirect_to new_import_google_code_path + return redirect_to new_import_google_code_path end @repos = client.repos + @incompatible_repos = client.incompatible_repos @already_added_projects = current_user.created_projects.where(import_type: "google_code") already_added_projects_names = @already_added_projects.pluck(:import_source) diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 1f97ff16c5..eb3c823353 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -1,11 +1,9 @@ class InvitesController < ApplicationController - before_filter :member - skip_before_filter :authenticate_user!, only: :decline + before_action :member + skip_before_action :authenticate_user!, only: :decline respond_to :html - layout 'navless' - def show end @@ -24,7 +22,7 @@ class InvitesController < ApplicationController if member.decline_invite! label, _ = source_info(member.source) - path = + path = if current_user dashboard_path else @@ -41,7 +39,7 @@ class InvitesController < ApplicationController def member return @member if defined?(@member) - + @token = params[:id] @member = Member.find_by_invite_token(@token) diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb index 386d103ee5..83eec1bf4a 100644 --- a/app/controllers/namespaces_controller.rb +++ b/app/controllers/namespaces_controller.rb @@ -1,5 +1,5 @@ class NamespacesController < ApplicationController - skip_before_filter :authenticate_user! + skip_before_action :authenticate_user! def show namespace = Namespace.find_by(path: params[:id]) diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index efa291d939..fc31118124 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,6 +1,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController - before_filter :authenticate_user! - layout "profile" + include Gitlab::CurrentSettings + include PageLayoutHelper + + before_action :verify_user_oauth_applications_enabled + before_action :authenticate_user! + + layout 'profile' def index head :forbidden and return @@ -10,7 +15,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController @application = Doorkeeper::Application.new(application_params) @application.owner = current_user - + if @application.save flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) redirect_to oauth_application_url(@application) @@ -29,6 +34,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController private + def verify_user_oauth_applications_enabled + return if current_application_settings.user_oauth_applications? + + redirect_to applications_profile_url + end + def set_application @application = current_user.oauth_applications.find(params[:id]) end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index a57b4a60c2..24025d8c72 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -1,6 +1,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController - before_filter :authenticate_resource_owner! - layout "profile" + before_action :authenticate_resource_owner! + + layout 'profile' def new if pre_auth.authorizable? diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 0b27ce7da7..4193ac1139 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -1,8 +1,15 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController - layout "profile" + include PageLayoutHelper + + layout 'profile' def destroy - Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner) + if params[:token_id].present? + current_resource_owner.oauth_authorized_tokens.find(params[:token_id]).revoke + else + Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner) + end + redirect_to applications_profile_url, notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy]) end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index bb9d65c9ed..523264b8ea 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,4 +1,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController + + protect_from_forgery except: [:kerberos, :saml] + Gitlab.config.omniauth.providers.each do |provider| define_method provider['name'] do handle_omniauth @@ -21,10 +24,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController @user = Gitlab::LDAP::User.new(oauth) @user.save if @user.changed? # will also save new users gl_user = @user.gl_user - gl_user.remember_me = true if @user.persisted? + gl_user.remember_me = params[:remember_me] if @user.persisted? # Do additional LDAP checks for the user filter and EE features if @user.allowed? + log_audit_event(gl_user, with: :ldap) sign_in_and_redirect(gl_user) else flash[:alert] = "Access denied for your LDAP account." @@ -44,6 +48,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if current_user # Add new authentication method current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) + log_audit_event(current_user, with: oauth['provider']) redirect_to profile_account_path, notice: 'Authentication method updated' else @user = Gitlab::OAuth::User.new(oauth) @@ -51,6 +56,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController # Only allow properly saved users to login. if @user.persisted? && @user.valid? + log_audit_event(@user.gl_user, with: oauth['provider']) sign_in_and_redirect(@user.gl_user) else error_message = @@ -65,12 +71,25 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return end end - rescue Gitlab::OAuth::ForbiddenAction => e - flash[:notice] = e.message + rescue Gitlab::OAuth::SignupDisabledError => e + label = Gitlab::OAuth::Provider.label_for(oauth['provider']) + message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." + + if current_application_settings.signup_enabled? + message << " Create a GitLab account first, and then connect it to your #{label} account." + end + + flash[:notice] = message + redirect_to new_user_session_path end def oauth @oauth ||= request.env['omniauth.auth'] end + + def log_audit_event(user, options = {}) + AuditEventService.new(user, user, options). + for_authentication.security_event + end end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index dcbbe5baa4..8450ba3102 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -15,4 +15,45 @@ class PasswordsController < Devise::PasswordsController respond_with(resource) end end + + # After a user resets their password, prompt for 2FA code if enabled instead + # of signing in automatically + # + # See http://git.io/vURrI + def update + super do |resource| + # TODO (rspeicher): In Devise master (> 3.4.1), we can set + # `Devise.sign_in_after_reset_password = false` and avoid this mess. + if resource.errors.empty? && resource.try(:two_factor_enabled?) + resource.unlock_access! if unlockable?(resource) + + # Since we are not signing this user in, we use the :updated_not_active + # message which only contains "Your password was changed successfully." + set_flash_message(:notice, :updated_not_active) if is_flashing_format? + + # Redirect to sign in so they can enter 2FA code + respond_with(resource, location: new_session_path(resource)) and return + end + end + end + + def edit + super + reset_password_token = Devise.token_generator.digest( + User, + :reset_password_token, + resource.reset_password_token + ) + + unless reset_password_token.nil? + user = User.where( + reset_password_token: reset_password_token + ).first_or_initialize + + unless user.reset_password_period_valid? + flash[:alert] = 'Your password reset token has expired.' + redirect_to(new_user_password_url(user_email: user['email'])) + end + end + end end diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 9bd34fe226..175afbf842 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -1,6 +1,4 @@ -class Profiles::AccountsController < ApplicationController - layout "profile" - +class Profiles::AccountsController < Profiles::ApplicationController def show @user = current_user end diff --git a/app/controllers/profiles/application_controller.rb b/app/controllers/profiles/application_controller.rb new file mode 100644 index 0000000000..c8be288b9a --- /dev/null +++ b/app/controllers/profiles/application_controller.rb @@ -0,0 +1,3 @@ +class Profiles::ApplicationController < ApplicationController + layout 'profile' +end diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb index 57f3bbf062..f193adb46b 100644 --- a/app/controllers/profiles/avatars_controller.rb +++ b/app/controllers/profiles/avatars_controller.rb @@ -1,6 +1,4 @@ -class Profiles::AvatarsController < ApplicationController - layout "profile" - +class Profiles::AvatarsController < Profiles::ApplicationController def destroy @user = current_user @user.remove_avatar! diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 954c98c0d9..0ede9b8e21 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -1,16 +1,17 @@ -class Profiles::EmailsController < ApplicationController - layout "profile" - +class Profiles::EmailsController < Profiles::ApplicationController def index @primary = current_user.email - @public_email = current_user.public_email @emails = current_user.emails end def create @email = current_user.emails.new(email_params) - flash[:alert] = @email.errors.full_messages.first unless @email.save + if @email.save + NotificationService.new.new_email(@email) + else + flash[:alert] = @email.errors.full_messages.first + end redirect_to profile_emails_url end @@ -19,9 +20,7 @@ class Profiles::EmailsController < ApplicationController @email = current_user.emails.find(params[:id]) @email.destroy - current_user.set_notification_email - current_user.set_public_email - current_user.save if current_user.notification_email_changed? or current_user.public_email_changed? + current_user.update_secondary_emails! respond_to do |format| format.html { redirect_to profile_emails_url } diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 4e2bd0a9b4..f3224148fd 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -1,6 +1,5 @@ -class Profiles::KeysController < ApplicationController - layout "profile" - skip_before_filter :authenticate_user!, only: [:get_keys] +class Profiles::KeysController < Profiles::ApplicationController + skip_before_action :authenticate_user!, only: [:get_keys] def index @keys = current_user.keys diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 3fdcbbab61..22423651c1 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,6 +1,4 @@ -class Profiles::NotificationsController < ApplicationController - layout 'profile' - +class Profiles::NotificationsController < Profiles::ApplicationController def show @user = current_user @notification = current_user.notification diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index 0c614969a3..c780e0983f 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -1,12 +1,11 @@ -class Profiles::PasswordsController < ApplicationController +class Profiles::PasswordsController < Profiles::ApplicationController + skip_before_action :check_password_expiration, only: [:new, :create] + + before_action :set_user + before_action :authorize_change_password! + layout :determine_layout - skip_before_filter :check_password_expiration, only: [:new, :create] - - before_filter :set_user - before_filter :set_title - before_filter :authorize_change_password! - def new end @@ -66,13 +65,9 @@ class Profiles::PasswordsController < ApplicationController @user = current_user end - def set_title - @title = "New password" - end - def determine_layout if [:new, :create].include?(action_name.to_sym) - 'navless' + 'application' else 'profile' end diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb new file mode 100644 index 0000000000..f83b4abd1e --- /dev/null +++ b/app/controllers/profiles/preferences_controller.rb @@ -0,0 +1,39 @@ +class Profiles::PreferencesController < Profiles::ApplicationController + before_action :user + + def show + end + + def update + begin + if @user.update_attributes(preferences_params) + flash[:notice] = 'Preferences saved.' + else + flash[:alert] = 'Failed to save preferences.' + end + rescue ArgumentError => e + # Raised when `dashboard` is given an invalid value. + flash[:alert] = "Failed to save preferences (#{e.message})." + end + + respond_to do |format| + format.html { redirect_to profile_preferences_path } + format.js + end + end + + private + + def user + @user = current_user + end + + def preferences_params + params.require(:user).permit( + :color_scheme_id, + :dashboard, + :project_view, + :theme_id + ) + end +end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb new file mode 100644 index 0000000000..f9af0871cf --- /dev/null +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -0,0 +1,48 @@ +class Profiles::TwoFactorAuthsController < Profiles::ApplicationController + def new + unless current_user.otp_secret + current_user.otp_secret = User.generate_otp_secret(32) + current_user.save! + end + + @qr_code = build_qr_code + end + + def create + if current_user.valid_otp?(params[:pin_code]) + current_user.two_factor_enabled = true + @codes = current_user.generate_otp_backup_codes! + current_user.save! + + render 'create' + else + @error = 'Invalid pin code' + @qr_code = build_qr_code + + render 'new' + end + end + + def codes + @codes = current_user.generate_otp_backup_codes! + current_user.save! + end + + def destroy + current_user.disable_two_factor! + + redirect_to profile_account_path + end + + private + + def build_qr_code + issuer = "#{issuer_host} | #{current_user.email}" + uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) + RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) + end + + def issuer_host + Gitlab.config.gitlab.host + end +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 7f76906066..26a4de1546 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,22 +1,18 @@ -class ProfilesController < ApplicationController +class ProfilesController < Profiles::ApplicationController include ActionView::Helpers::SanitizeHelper - before_filter :user - before_filter :authorize_change_username!, only: :update_username - skip_before_filter :require_email, only: [:show, :update] - - layout 'profile' + before_action :user + before_action :authorize_change_username!, only: :update_username + skip_before_action :require_email, only: [:show, :update] def show end - def design - end - def applications @applications = current_user.oauth_applications @authorized_tokens = current_user.oauth_authorized_tokens - @authorized_apps = @authorized_tokens.map(&:application).uniq + @authorized_anonymous_tokens = @authorized_tokens.reject(&:application) + @authorized_apps = @authorized_tokens.map(&:application).uniq - [nil] end def update @@ -31,7 +27,6 @@ class ProfilesController < ApplicationController respond_to do |format| format.html { redirect_to :back } - format.js end end @@ -43,8 +38,11 @@ class ProfilesController < ApplicationController redirect_to profile_account_path end - def history - @events = current_user.recent_events.page(params[:page]).per(PER_PAGE) + def audit_log + @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id). + order("created_at DESC"). + page(params[:page]). + per(PER_PAGE) end def update_username @@ -67,10 +65,21 @@ class ProfilesController < ApplicationController def user_params params.require(:user).permit( - :email, :password, :password_confirmation, :bio, :name, - :username, :skype, :linkedin, :twitter, :website_url, - :color_scheme_id, :theme_id, :avatar, :hide_no_ssh_key, - :hide_no_password, :location, :public_email + :avatar, + :bio, + :email, + :hide_no_password, + :hide_no_ssh_key, + :linkedin, + :location, + :name, + :password, + :password_confirmation, + :public_email, + :skype, + :twitter, + :username, + :website_url ) end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 4719933394..ee88d49b40 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,7 +1,7 @@ class Projects::ApplicationController < ApplicationController - before_filter :project - before_filter :repository - layout :determine_layout + before_action :project + before_action :repository + layout 'project' def authenticate_user! # Restrict access to Projects area only @@ -17,14 +17,6 @@ class Projects::ApplicationController < ApplicationController super end - def determine_layout - if current_user - 'projects' - else - 'public_projects' - end - end - def require_branch_head unless @repository.branch_names.include?(@ref) redirect_to( diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index a482b90880..9c3763d593 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -1,7 +1,5 @@ class Projects::AvatarsController < Projects::ApplicationController - layout 'project' - - before_filter :project + before_action :project def show @blob = @project.repository.blob_at_branch('master', @project.avatar_in_git) diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index a87b8270a2..9ea518e6c8 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -2,12 +2,34 @@ class Projects::BlameController < Projects::ApplicationController include ExtractsPath - before_filter :require_non_empty_project - before_filter :assign_ref_vars - before_filter :authorize_download_code! + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show - @blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path) - @blob = @blame.blob + @blob = @repository.blob_at(@commit.id, @path) + @blame = group_blame_lines + end + + def group_blame_lines + blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path) + + prev_sha = nil + groups = [] + current_group = nil + + blame.each do |commit, line| + if prev_sha && prev_sha == commit.sha + current_group[:lines] << line + else + groups << current_group if current_group.present? + current_group = { commit: commit, lines: [line] } + end + + prev_sha = commit.sha + end + + groups << current_group if current_group.present? + groups end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 4b7eb4df29..b762518d37 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -6,15 +6,15 @@ class Projects::BlobController < Projects::ApplicationController # Raised when given an invalid file path class InvalidPathError < StandardError; end - before_filter :require_non_empty_project, except: [:new, :create] - before_filter :authorize_download_code! - before_filter :authorize_push_code!, only: [:destroy] - before_filter :assign_blob_vars - before_filter :commit, except: [:new, :create] - before_filter :blob, except: [:new, :create] - before_filter :from_merge_request, only: [:edit, :update] - before_filter :after_edit_path, only: [:edit, :update] - before_filter :require_branch_head, only: [:edit, :update] + before_action :require_non_empty_project, except: [:new, :create] + before_action :authorize_download_code! + before_action :authorize_push_code!, only: [:destroy] + before_action :assign_blob_vars + before_action :commit, except: [:new, :create] + before_action :blob, except: [:new, :create] + before_action :from_merge_request, only: [:edit, :update] + before_action :after_edit_path, only: [:edit, :update] + before_action :require_branch_head, only: [:edit, :update] def new commit unless @repository.empty? diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index f049e96e61..3ac0a75fa7 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -1,9 +1,9 @@ class Projects::BranchesController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper # Authorize - before_filter :require_non_empty_project - before_filter :authorize_download_code! - before_filter :authorize_push_code!, only: [:create, :destroy] + before_action :require_non_empty_project + before_action :authorize_download_code! + before_action :authorize_push_code!, only: [:create, :destroy] def index @sort = params[:sort] || 'name' @@ -17,7 +17,9 @@ class Projects::BranchesController < Projects::ApplicationController def create branch_name = sanitize(strip_tags(params[:branch_name])) + branch_name = Addressable::URI.unescape(branch_name) ref = sanitize(strip_tags(params[:ref])) + ref = Addressable::URI.unescape(ref) result = CreateBranchService.new(project, current_user). execute(branch_name, ref) @@ -32,15 +34,14 @@ class Projects::BranchesController < Projects::ApplicationController end def destroy - DeleteBranchService.new(project, current_user).execute(params[:id]) - @branch_name = params[:id] - + @branch_name = Addressable::URI.unescape(params[:id]) + status = DeleteBranchService.new(project, current_user).execute(@branch_name) respond_to do |format| format.html do redirect_to namespace_project_branches_path(@project.namespace, @project) end - format.js + format.js { render status: status[:return_code] } end end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 87e39f1363..78d42d695b 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -3,18 +3,18 @@ # Not to be confused with CommitsController, plural. class Projects::CommitController < Projects::ApplicationController # Authorize - before_filter :require_non_empty_project - before_filter :authorize_download_code! - before_filter :commit + before_action :require_non_empty_project + before_action :authorize_download_code! + before_action :commit def show return git_not_found! unless @commit - @line_notes = @project.notes.for_commit_id(commit.id).inline + @line_notes = commit.notes.inline @diffs = @commit.diffs @note = @project.build_commit_note(commit) - @notes_count = @project.notes.for_commit_id(commit.id).count - @notes = @project.notes.for_commit_id(@commit.id).not_inline.fresh + @notes_count = commit.notes.count + @notes = commit.notes.not_inline.fresh @noteable = @commit @comments_allowed = @reply_allowed = true @comments_target = { @@ -36,6 +36,6 @@ class Projects::CommitController < Projects::ApplicationController end def commit - @commit ||= @project.repository.commit(params[:id]) + @commit ||= @project.commit(params[:id]) end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 4b6ab43747..d1c15174ae 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -3,9 +3,9 @@ require "base64" class Projects::CommitsController < Projects::ApplicationController include ExtractsPath - before_filter :require_non_empty_project - before_filter :assign_ref_vars - before_filter :authorize_download_code! + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show @repo = @project.repository diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 146808fa56..c5f085c236 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -1,14 +1,17 @@ +require 'addressable/uri' + class Projects::CompareController < Projects::ApplicationController # Authorize - before_filter :require_non_empty_project - before_filter :authorize_download_code! + before_action :require_non_empty_project + before_action :authorize_download_code! def index + @ref = Addressable::URI.unescape(params[:to]) end def show - base_ref = params[:from] - head_ref = params[:to] + base_ref = Addressable::URI.unescape(params[:from]) + @ref = head_ref = Addressable::URI.unescape(params[:to]) compare_result = CompareService.new.execute( current_user, diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 6fba3ce299..40e2b37912 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -2,7 +2,7 @@ class Projects::DeployKeysController < Projects::ApplicationController respond_to :html # Authorize - before_filter :authorize_admin_project! + before_action :authorize_admin_project! layout "project_settings" @@ -18,10 +18,6 @@ class Projects::DeployKeysController < Projects::ApplicationController @available_public_keys -= @available_project_keys end - def show - @key = @project.deploy_keys.find(params[:id]) - end - def new @key = @project.deploy_keys.new diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 21a151a426..9e72597ea8 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -1,7 +1,7 @@ class Projects::ForksController < Projects::ApplicationController # Authorize - before_filter :require_non_empty_project - before_filter :authorize_download_code! + before_action :require_non_empty_project + before_action :authorize_download_code! def new @namespaces = current_user.manageable_namespaces @@ -18,7 +18,6 @@ class Projects::ForksController < Projects::ApplicationController notice: 'Project was successfully forked.' ) else - @title = 'Fork project' render :error end end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 6e54af356e..0b6f7f5c91 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -1,7 +1,10 @@ class Projects::GraphsController < Projects::ApplicationController + include ExtractsPath + # Authorize - before_filter :require_non_empty_project - before_filter :authorize_download_code! + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show respond_to do |format| @@ -13,7 +16,7 @@ class Projects::GraphsController < Projects::ApplicationController end def commits - @commits = @project.repository.commits(nil, nil, 2000, 0, true) + @commits = @project.repository.commits(@ref, nil, 2000, 0, true) @commits_graph = Gitlab::Graphs::Commits.new(@commits) @commits_per_week_days = @commits_graph.commits_per_week_days @commits_per_time = @commits_graph.commits_per_time @@ -23,7 +26,7 @@ class Projects::GraphsController < Projects::ApplicationController private def fetch_graph - @commits = @project.repository.commits(nil, nil, 6000, 0, true) + @commits = @project.repository.commits(@ref, nil, 6000, 0, true) @log = [] @commits.each do |commit| diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index ba95bb13e1..76062446c9 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -1,6 +1,6 @@ class Projects::HooksController < Projects::ApplicationController # Authorize - before_filter :authorize_admin_project! + before_action :authorize_admin_project! respond_to :html @@ -53,6 +53,6 @@ class Projects::HooksController < Projects::ApplicationController end def hook_params - params.require(:hook).permit(:url, :push_events, :issues_events, :merge_requests_events, :tag_push_events) + params.require(:hook).permit(:url, :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events) end end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index b64491b466..066b66014f 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -1,8 +1,8 @@ class Projects::ImportsController < Projects::ApplicationController # Authorize - before_filter :authorize_admin_project! - before_filter :require_no_repo - before_filter :redirect_if_progress, except: :show + before_action :authorize_admin_project! + before_action :require_no_repo + before_action :redirect_if_progress, except: :show def new end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 88302276b5..0f89f2e88c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,25 +1,33 @@ class Projects::IssuesController < Projects::ApplicationController - before_filter :module_enabled - before_filter :issue, only: [:edit, :update, :show, :toggle_subscription] + before_action :module_enabled + before_action :issue, only: [:edit, :update, :show, :toggle_subscription] # Allow read any issue - before_filter :authorize_read_issue! + before_action :authorize_read_issue! # Allow write(create) issue - before_filter :authorize_write_issue!, only: [:new, :create] + before_action :authorize_create_issue!, only: [:new, :create] # Allow modify issue - before_filter :authorize_modify_issue!, only: [:edit, :update] + before_action :authorize_update_issue!, only: [:edit, :update] # Allow issues bulk update - before_filter :authorize_admin_issues!, only: [:bulk_update] + before_action :authorize_admin_issues!, only: [:bulk_update] respond_to :html def index terms = params['issue_search'] @issues = get_issues_collection - @issues = @issues.full_search(terms) if terms.present? + + if terms.present? + if terms =~ /\A#(\d+)\z/ + @issues = @issues.where(iid: $1) + else + @issues = @issues.full_search(terms) + end + end + @issues = @issues.page(params[:page]).per(PER_PAGE) respond_to do |format| @@ -47,6 +55,7 @@ class Projects::IssuesController < Projects::ApplicationController end def show + @participants = @issue.participants(current_user, @project) @note = @project.notes.new(noteable: @issue) @notes = @issue.notes.inc_author.fresh @noteable = @issue @@ -99,7 +108,7 @@ class Projects::IssuesController < Projects::ApplicationController def toggle_subscription @issue.toggle_subscription(current_user) - + render nothing: true end @@ -113,8 +122,8 @@ class Projects::IssuesController < Projects::ApplicationController end end - def authorize_modify_issue! - return render_404 unless can?(current_user, :modify_issue, @issue) + def authorize_update_issue! + return render_404 unless can?(current_user, :update_issue, @issue) end def authorize_admin_issues! @@ -122,7 +131,7 @@ class Projects::IssuesController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.issues_enabled + return render_404 unless @project.issues_enabled && @project.default_issues_tracker? end # Since iids are implemented only in 6.1 diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 207a01ed3b..86d6e3e0f6 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -1,8 +1,8 @@ class Projects::LabelsController < Projects::ApplicationController - before_filter :module_enabled - before_filter :label, only: [:edit, :update, :destroy] - before_filter :authorize_labels! - before_filter :authorize_admin_labels!, except: [:index] + before_action :module_enabled + before_action :label, only: [:edit, :update, :destroy] + before_action :authorize_read_label! + before_action :authorize_admin_labels!, except: [:index] respond_to :js, :html diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 47ce846735..d126519831 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,25 +1,36 @@ require 'gitlab/satellite/satellite' class Projects::MergeRequestsController < Projects::ApplicationController - before_filter :module_enabled - before_filter :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status, :toggle_subscription] - before_filter :closes_issues, only: [:edit, :update, :show, :diffs] - before_filter :validates_merge_request, only: [:show, :diffs] - before_filter :define_show_vars, only: [:show, :diffs] + before_action :module_enabled + before_action :merge_request, only: [ + :edit, :update, :show, :diffs, :commits, :automerge, :automerge_check, + :ci_status, :toggle_subscription + ] + before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] + before_action :validates_merge_request, only: [:show, :diffs, :commits] + before_action :define_show_vars, only: [:show, :diffs, :commits] # Allow read any merge_request - before_filter :authorize_read_merge_request! + before_action :authorize_read_merge_request! # Allow write(create) merge_request - before_filter :authorize_write_merge_request!, only: [:new, :create] + before_action :authorize_create_merge_request!, only: [:new, :create] # Allow modify merge_request - before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort] + before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :sort] def index terms = params['issue_search'] @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.full_search(terms) if terms.present? + + if terms.present? + if terms =~ /\A[#!](\d+)\z/ + @merge_requests = @merge_requests.where(iid: $1) + else + @merge_requests = @merge_requests.full_search(terms) + end + end + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) respond_to do |format| @@ -59,6 +70,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def commits + respond_to do |format| + format.html { render 'show' } + format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } } + end + end + def new params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @@ -124,13 +142,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.check_if_can_be_merged end - render json: { merge_status: @merge_request.merge_status_name } + closes_issues + + render partial: "projects/merge_requests/widget/show.html.haml", layout: false end def automerge - return access_denied! unless allowed_to_merge? + return access_denied! unless @merge_request.can_be_merged_by?(current_user) - if @merge_request.open? && @merge_request.can_be_merged? + if @merge_request.automergeable? AutoMergeWorker.perform_async(@merge_request.id, current_user.id, params) @status = true else @@ -146,7 +166,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def branch_to @target_project = selected_target_project - @commit = @target_project.repository.commit(params[:ref]) if params[:ref].present? + @commit = @target_project.commit(params[:ref]) if params[:ref].present? end def update_branches @@ -176,7 +196,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def toggle_subscription @merge_request.toggle_subscription(current_user) - + render nothing: true end @@ -198,8 +218,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @closes_issues ||= @merge_request.closes_issues end - def authorize_modify_merge_request! - return render_404 unless can?(current_user, :modify_merge_request, @merge_request) + def authorize_update_merge_request! + return render_404 unless can?(current_user, :update_merge_request, @merge_request) end def authorize_admin_merge_request! @@ -226,6 +246,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_show_vars + @participants = @merge_request.participants(current_user, @project) + # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) @notes = @merge_request.mr_and_commit_notes.inc_author.fresh @@ -237,8 +259,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.commits @merge_request_diff = @merge_request.merge_request_diff - @allowed_to_merge = allowed_to_merge? - @show_merge_controls = @merge_request.open? && @commits.any? && @allowed_to_merge @source_branch = @merge_request.source_project.repository.find_branch(@merge_request.source_branch).try(:name) if @merge_request.locked_long_ago? @@ -247,19 +267,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def allowed_to_merge? - allowed_to_push_code?(project, @merge_request.target_branch) - end - def invalid_mr # Render special view for MR with removed source or target branch render 'invalid' end - def allowed_to_push_code?(project, branch) - ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch) - end - def merge_request_params params.require(:merge_request).permit( :title, :assignee_id, :source_project_id, :source_branch, diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index b49b549547..9efe9704d1 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,12 +1,12 @@ class Projects::MilestonesController < Projects::ApplicationController - before_filter :module_enabled - before_filter :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests] + before_action :module_enabled + before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests] # Allow read any milestone - before_filter :authorize_read_milestone! + before_action :authorize_read_milestone! # Allow admin milestone - before_filter :authorize_admin_milestone!, except: [:index, :show] + before_action :authorize_admin_milestone!, except: [:index, :show] respond_to :html @@ -64,7 +64,12 @@ class Projects::MilestonesController < Projects::ApplicationController end def destroy - return access_denied! unless can?(current_user, :admin_milestone, @milestone) + return access_denied! unless can?(current_user, :admin_milestone, @project) + + update_params = { milestone: nil } + @milestone.issues.each do |issue| + Issues::UpdateService.new(@project, current_user, update_params).execute(issue) + end @milestone.destroy diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 83d1c1daca..b181c47bae 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -2,11 +2,15 @@ class Projects::NetworkController < Projects::ApplicationController include ExtractsPath include ApplicationHelper - before_filter :require_non_empty_project - before_filter :assign_ref_vars - before_filter :authorize_download_code! + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show + + @url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json)) + @commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s") + respond_to do |format| format.html diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 868629a0bc..0f5d82ce13 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,9 +1,9 @@ class Projects::NotesController < Projects::ApplicationController # Authorize - before_filter :authorize_read_note! - before_filter :authorize_write_note!, only: [:create] - before_filter :authorize_admin_note!, only: [:update, :destroy] - before_filter :find_current_user_notes, except: [:destroy, :delete_attachment] + before_action :authorize_read_note! + before_action :authorize_create_note!, only: [:create] + before_action :authorize_admin_note!, only: [:update, :destroy] + before_action :find_current_user_notes, except: [:destroy, :delete_attachment] def index current_fetched_at = Time.now.to_i @@ -30,13 +30,10 @@ class Projects::NotesController < Projects::ApplicationController end def update - if note.editable? - note.update_attributes(note_params) - note.reset_events_cache - end + @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) respond_to do |format| - format.json { render_note_json(note) } + format.json { render_note_json(@note) } format.html { redirect_to :back } end end @@ -77,11 +74,24 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_html(note) + if params[:view] == 'parallel' + template = "projects/notes/_diff_notes_with_reply_parallel" + locals = + if params[:line_type] == 'old' + { notes_left: [note], notes_right: [] } + else + { notes_left: [], notes_right: [note] } + end + else + template = "projects/notes/_diff_notes_with_reply" + locals = { notes: [note] } + end + render_to_string( - "projects/notes/_diff_notes_with_reply", + template, layout: false, formats: [:html], - locals: { notes: [note] } + locals: locals ) end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 72967a26ff..b82b6f45d5 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,8 +1,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize - before_filter :authorize_admin_project!, except: :leave - - layout "project_settings" + before_action :authorize_admin_project!, except: :leave def index @project_members = @project.project_members @@ -24,7 +22,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController users = @group.users.search(params[:search]).to_a @group_members = @group_members.where(user_id: users) end - + @group_members = @group_members.order('access_level DESC').limit(20) end @@ -62,7 +60,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController redirect_path = namespace_project_project_members_path(@project.namespace, @project) @project_member = @project.project_members.find(params[:id]) - + if @project_member.invite? @project_member.resend_invite @@ -73,10 +71,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def leave + if @project.namespace == current_user.namespace + return redirect_to(:back, alert: 'You can not leave your own project. Transfer or delete the project.') + end + @project.project_members.find_by(user_id: current_user).destroy respond_to do |format| - format.html { redirect_to :back } + format.html { redirect_to dashboard_path } format.js { render nothing: true } end end diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index ac36ac6fcd..6b52eccebf 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,7 +1,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController # Authorize - before_filter :require_non_empty_project - before_filter :authorize_admin_project! + before_action :require_non_empty_project + before_action :authorize_admin_project! layout "project_settings" diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index b1a029ce69..647c145407 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -2,9 +2,9 @@ class Projects::RawController < Projects::ApplicationController include ExtractsPath - before_filter :require_non_empty_project - before_filter :assign_ref_vars - before_filter :authorize_download_code! + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def show @blob = @repository.blob_at(@commit.id, @path) diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index ec3b2b8d75..6080c849c8 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -1,24 +1,29 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath + include TreeHelper - before_filter :require_non_empty_project - before_filter :assign_ref_vars - before_filter :authorize_download_code! + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! def switch respond_to do |format| format.html do - new_path = if params[:destination] == "tree" - namespace_project_tree_path(@project.namespace, @project, - (@id)) - elsif params[:destination] == "blob" - namespace_project_blob_path(@project.namespace, @project, - (@id)) - elsif params[:destination] == "graph" - namespace_project_network_path(@project.namespace, @project, @id, @options) - else - namespace_project_commits_path(@project.namespace, @project, @id) - end + new_path = + case params[:destination] + when "tree" + namespace_project_tree_path(@project.namespace, @project, @id) + when "blob" + namespace_project_blob_path(@project.namespace, @project, @id) + when "graph" + namespace_project_network_path(@project.namespace, @project, @id, @options) + when "graphs" + namespace_project_graph_path(@project.namespace, @project, @id) + when "graphs_commits" + commits_namespace_project_graph_path(@project.namespace, @project, @id) + else + namespace_project_commits_path(@project.namespace, @project, @id) + end redirect_to new_path end @@ -56,6 +61,11 @@ class Projects::RefsController < Projects::ApplicationController } end + if @logs.present? + @log_url = namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/')) + @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit)) + end + respond_to do |format| format.html { render_404 } format.js diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 96defb0c72..c4a5e2d635 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -1,8 +1,8 @@ class Projects::RepositoriesController < Projects::ApplicationController # Authorize - before_filter :require_non_empty_project, except: :create - before_filter :authorize_download_code! - before_filter :authorize_admin_project!, only: :create + before_action :require_non_empty_project, except: :create + before_action :authorize_download_code! + before_action :authorize_admin_project!, only: :create def create @project.create_repository diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 9a484c109b..0110553247 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -1,7 +1,17 @@ class Projects::ServicesController < Projects::ApplicationController + ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain, + :room, :recipients, :project_url, :webhook, + :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, + :build_key, :server, :teamcity_url, :build_type, + :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, + :colorize_messages, :channels, + :push_events, :issues_events, :merge_requests_events, :tag_push_events, + :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url, + :notify, :color, + :server_host, :server_port, :default_irc_uri] # Authorize - before_filter :authorize_admin_project! - before_filter :service, only: [:edit, :update, :test] + before_action :authorize_admin_project! + before_action :service, only: [:edit, :update, :test] respond_to :html @@ -29,10 +39,13 @@ class Projects::ServicesController < Projects::ApplicationController def test data = Gitlab::PushDataBuilder.build_sample(project, current_user) - if @service.execute(data) + outcome = @service.test(data) + if outcome[:success] message = { notice: 'We sent a request to the provided URL' } else - message = { alert: 'We tried to send a request to the provided URL but an error occured' } + error_message = "We tried to send a request to the provided URL but an error occurred" + error_message << ": #{outcome[:result]}" if outcome[:result].present? + message = { alert: error_message } end redirect_to :back, message @@ -45,15 +58,6 @@ class Projects::ServicesController < Projects::ApplicationController end def service_params - params.require(:service).permit( - :title, :token, :type, :active, :api_key, :subdomain, - :room, :recipients, :project_url, :webhook, - :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, - :build_key, :server, :teamcity_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, - :colorize_messages, :channels, - :push_events, :issues_events, :merge_requests_events, :tag_push_events, - :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url - ) + params.require(:service).permit(ALLOWED_PARAMS) end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index ed26840037..6430663742 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,18 +1,18 @@ class Projects::SnippetsController < Projects::ApplicationController - before_filter :module_enabled - before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :module_enabled + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read any snippet - before_filter :authorize_read_project_snippet! + before_action :authorize_read_project_snippet! # Allow write(create) snippet - before_filter :authorize_write_project_snippet!, only: [:new, :create] + before_action :authorize_create_project_snippet!, only: [:new, :create] # Allow modify snippet - before_filter :authorize_modify_project_snippet!, only: [:edit, :update] + before_action :authorize_update_project_snippet!, only: [:edit, :update] # Allow destroy snippet - before_filter :authorize_admin_project_snippet!, only: [:destroy] + before_action :authorize_admin_project_snippet!, only: [:destroy] respond_to :html @@ -75,8 +75,8 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet ||= @project.snippets.find(params[:id]) end - def authorize_modify_project_snippet! - return render_404 unless can?(current_user, :modify_project_snippet, @snippet) + def authorize_update_project_snippet! + return render_404 unless can?(current_user, :update_project_snippet, @snippet) end def authorize_admin_project_snippet! diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 83f4937bce..f565fbbbbc 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -1,9 +1,9 @@ class Projects::TagsController < Projects::ApplicationController # Authorize - before_filter :require_non_empty_project - before_filter :authorize_download_code! - before_filter :authorize_push_code!, only: [:create] - before_filter :authorize_admin_project!, only: [:destroy] + before_action :require_non_empty_project + before_action :authorize_download_code! + before_action :authorize_push_code!, only: [:create] + before_action :authorize_admin_project!, only: [:destroy] def index sorted = VersionSorter.rsort(@repository.tag_names) diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index b23010bf59..92e4bc16d9 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -2,18 +2,20 @@ class Projects::TreeController < Projects::ApplicationController include ExtractsPath - before_filter :require_non_empty_project, except: [:new, :create] - before_filter :assign_ref_vars - before_filter :authorize_download_code! + before_action :require_non_empty_project, except: [:new, :create] + before_action :assign_ref_vars + before_action :authorize_download_code! def show + return not_found! unless @repository.commit(@ref) + if tree.entries.empty? if @repository.blob_at(@commit.id, @path) redirect_to( namespace_project_blob_path(@project.namespace, @project, File.join(@ref, @path)) ) and return - else + elsif @path.present? return not_found! end end diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 276dced865..71ecc20dd9 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -1,11 +1,6 @@ class Projects::UploadsController < Projects::ApplicationController - layout 'project' - - # We want to skip these filters for only the `show` action if `image?` is true, - # but `skip_before_filter` doesn't work with both `only` and `if`, so we accomplish the same like this. - skipped_filters = [:authenticate_user!, :reject_blocked!, :project, :repository] - skip_before_filter *skipped_filters, only: [:show] - before_filter *skipped_filters, only: [:show], unless: :image? + skip_before_action :authenticate_user!, :reject_blocked!, :project, + :repository, if: -> { action_name == 'show' && image? } def create link_to_file = ::Projects::UploadService.new(project, params[:file]). @@ -40,7 +35,7 @@ class Projects::UploadsController < Projects::ApplicationController file_project = Project.find_with_namespace("#{namespace}/#{id}") if file_project.nil? - @uploader = nil + @uploader = nil return end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index aeb7f0699f..50512cb6dc 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,10 +1,10 @@ require 'project_wiki' class Projects::WikisController < Projects::ApplicationController - before_filter :authorize_read_wiki! - before_filter :authorize_write_wiki!, only: [:edit, :create, :history] - before_filter :authorize_admin_wiki!, only: :destroy - before_filter :load_project_wiki + before_action :authorize_read_wiki! + before_action :authorize_create_wiki!, only: [:edit, :create, :history] + before_action :authorize_admin_wiki!, only: :destroy + before_action :load_project_wiki include WikiHelper def pages @@ -28,7 +28,7 @@ class Projects::WikisController < Projects::ApplicationController ) end else - return render('empty') unless can?(current_user, :write_wiki, @project) + return render('empty') unless can?(current_user, :create_wiki, @project) @page = WikiPage.new(@project_wiki) @page.title = params[:id] @@ -43,7 +43,7 @@ class Projects::WikisController < Projects::ApplicationController def update @page = @project_wiki.find_page(params[:id]) - return render('empty') unless can?(current_user, :write_wiki, @project) + return render('empty') unless can?(current_user, :create_wiki, @project) if @page.update(content, format, message) redirect_to( diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 0f28794b73..dafc11d070 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,22 +1,21 @@ class ProjectsController < ApplicationController prepend_before_filter :render_go_import, only: [:show] - skip_before_filter :authenticate_user!, only: [:show] - before_filter :project, except: [:new, :create] - before_filter :repository, except: [:new, :create] + skip_before_action :authenticate_user!, only: [:show, :activity] + before_action :project, except: [:new, :create] + before_action :repository, except: [:new, :create] # Authorize - before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] - before_filter :set_title, only: [:new, :create] - before_filter :event_filter, only: :show + before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] + before_action :event_filter, only: [:show, :activity] - layout 'navless', only: [:new, :create, :fork] + layout :determine_layout def new @project = Project.new end def edit - render 'edit', layout: 'project_settings' + render 'edit' end def create @@ -25,7 +24,7 @@ class ProjectsController < ApplicationController if @project.saved? redirect_to( project_path(@project), - notice: 'Project was successfully created.' + notice: "Project '#{@project.name}' was successfully created." ) else render 'new' @@ -37,26 +36,37 @@ class ProjectsController < ApplicationController respond_to do |format| if status - flash[:notice] = 'Project was successfully updated.' + flash[:notice] = "Project '#{@project.name}' was successfully updated." format.html do redirect_to( edit_project_path(@project), - notice: 'Project was successfully updated.' + notice: "Project '#{@project.name}' was successfully updated." ) end format.js else - format.html { render 'edit', layout: 'project_settings' } + format.html { render 'edit' } format.js end end end def transfer - transfer_params = params.permit(:new_namespace_id) - ::Projects::TransferService.new(project, current_user, transfer_params).execute - if @project.errors[:namespace_id].present? - flash[:alert] = @project.errors[:namespace_id].first + namespace = Namespace.find_by(id: params[:new_namespace_id]) + ::Projects::TransferService.new(project, current_user).execute(namespace) + + if @project.errors[:new_namespace].present? + flash[:alert] = @project.errors[:new_namespace].first + end + end + + def activity + respond_to do |format| + format.html + format.json do + load_events + pager_json('events/_events', @events.count) + end end end @@ -66,29 +76,22 @@ class ProjectsController < ApplicationController return end - limit = (params[:limit] || 20).to_i - - @show_star = !(current_user && current_user.starred?(@project)) - respond_to do |format| format.html do if @project.repository_exists? if @project.empty_repo? - render 'projects/empty', layout: user_layout + render 'projects/empty' else - @last_push = current_user.recent_push(@project.id) if current_user - render :show, layout: user_layout + render :show end else - render 'projects/no_repo', layout: user_layout + render 'projects/no_repo' end end - format.json do - @events = @project.events.recent - @events = event_filter.apply_filter(@events).with_associations - @events = @events.limit(limit).offset(params[:offset] || 0) - pager_json('events/_events', @events.count) + format.atom do + load_events + render layout: false end end end @@ -97,18 +100,15 @@ class ProjectsController < ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).execute + flash[:alert] = "Project '#{@project.name}' was deleted." - respond_to do |format| - format.html do - flash[:alert] = 'Project deleted.' - - if request.referer.include?('/admin') - redirect_to admin_namespaces_projects_path - else - redirect_to dashboard_path - end - end + if request.referer.include?('/admin') + redirect_to admin_namespaces_projects_path + else + redirect_to dashboard_path end + rescue Projects::DestroyService::DestroyError => ex + redirect_to edit_project_path(@project), alert: ex.message end def autocomplete_sources @@ -150,21 +150,43 @@ class ProjectsController < ApplicationController def toggle_star current_user.toggle_star(@project) @project.reload - render json: { star_count: @project.star_count } + + render json: { + html: view_to_html_string("projects/buttons/_star") + } end def markdown_preview - render text: view_context.markdown(params[:md_text]) + text = params[:text] + + ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext.analyze(text) + + render json: { + body: view_context.markdown(text), + references: { + users: ext.users.map(&:username) + } + } end private - def set_title - @title = 'New Project' + def determine_layout + if [:new, :create].include?(action_name.to_sym) + 'application' + elsif [:edit, :update].include?(action_name.to_sym) + 'project_settings' + else + 'project' + end end - def user_layout - current_user ? 'projects' : 'public_projects' + def load_events + @events = @project.events.recent + @events = event_filter.apply_filter(@events).with_associations + limit = (params[:limit] || 20).to_i + @events = @events.limit(limit).offset(params[:offset] || 0) end def project_params diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 38d116a4ee..3b3dc86cb6 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,12 +1,12 @@ class RegistrationsController < Devise::RegistrationsController - before_filter :signup_enabled? + before_action :signup_enabled? def new redirect_to(new_user_session_path) end def destroy - current_user.destroy + DeleteUserService.new(current_user).execute(current_user) respond_to do |format| format.html { redirect_to new_user_session_path, notice: "Account successfully removed." } diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb new file mode 100644 index 0000000000..fdfe00dc13 --- /dev/null +++ b/app/controllers/root_controller.rb @@ -0,0 +1,28 @@ +# RootController +# +# This controller exists solely to handle requests to `root_url`. When a user is +# logged in and has customized their `dashboard` setting, they will be +# redirected to their preferred location. +# +# For users who haven't customized the setting, we simply delegate to +# `DashboardController#show`, which is the default. +class RootController < DashboardController + before_action :redirect_to_custom_dashboard, only: [:show] + + def show + super + end + + private + + def redirect_to_custom_dashboard + return unless current_user + + case current_user.dashboard + when 'stars' + redirect_to starred_dashboard_projects_path + else + return + end + end +end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index c5828d0b2d..4e2ea6c571 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,23 +1,27 @@ class SearchController < ApplicationController include SearchHelper + layout 'search' + def show return if params[:search].nil? || params[:search].blank? + @search_term = params[:search] + if params[:project_id].present? @project = Project.find_by(id: params[:project_id]) @project = nil unless can?(current_user, :download_code, @project) end if params[:group_id].present? - @group = Group.find_by(id: params[:group_id]) + @group = Group.find_by(id: params[:group_id]) @group = nil unless can?(current_user, :read_group, @group) end - + @scope = params[:scope] @show_snippets = params[:snippets].eql? 'true' - @search_results = + @search_results = if @project unless %w(blobs notes issues merge_requests wiki_blobs). include?(@scope) @@ -37,6 +41,7 @@ class SearchController < ApplicationController end Search::GlobalService.new(current_user, params).execute end + @objects = @search_results.objects(@scope, params[:page]) end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 3f11d7afe6..8389f07a3b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,5 +1,45 @@ class SessionsController < Devise::SessionsController + include AuthenticatesWithTwoFactor + + prepend_before_action :authenticate_with_two_factor, only: [:create] + prepend_before_action :store_redirect_path, only: [:new] + before_action :auto_sign_in_with_provider, only: [:new] + def new + if Gitlab.config.ldap.enabled + @ldap_servers = Gitlab::LDAP::Config.servers + end + + super + end + + def create + super do |resource| + # User has successfully signed in, so clear any unused reset token + if resource.reset_password_token.present? + resource.update_attributes(reset_password_token: nil, + reset_password_sent_at: nil) + end + authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard" + log_audit_event(current_user, with: authenticated_with) + end + end + + private + + def user_params + params.require(:user).permit(:login, :password, :remember_me, :otp_attempt) + end + + def find_user + if user_params[:login] + User.by_login(user_params[:login]) + elsif user_params[:otp_attempt] && session[:otp_user_id] + User.find(session[:otp_user_id]) + end + end + + def store_redirect_path redirect_path = if request.referer.present? && (params['redirect_to_referer'] == 'yes') referer_uri = URI(request.referer) @@ -14,24 +54,55 @@ class SessionsController < Devise::SessionsController # Prevent a 'you are already signed in' message directly after signing: # we should never redirect to '/users/sign_in' after signing in successfully. - unless redirect_path == '/users/sign_in' + unless redirect_path == new_user_session_path store_location_for(:redirect, redirect_path) end - - if Gitlab.config.ldap.enabled - @ldap_servers = Gitlab::LDAP::Config.servers - end - - super end - def create - super do |resource| - # User has successfully signed in, so clear any unused reset tokens - if resource.reset_password_token.present? - resource.update_attributes(reset_password_token: nil, - reset_password_sent_at: nil) + def authenticate_with_two_factor + user = self.resource = find_user + + return unless user && user.two_factor_enabled? + + if user_params[:otp_attempt].present? && session[:otp_user_id] + if valid_otp_attempt?(user) + # Remove any lingering user data from login + session.delete(:otp_user_id) + + sign_in(user) and return + else + flash.now[:alert] = 'Invalid two-factor code.' + render :two_factor and return + end + else + if user && user.valid_password?(user_params[:password]) + prompt_for_two_factor(user) end end end + + def auto_sign_in_with_provider + provider = Gitlab.config.omniauth.auto_sign_in_with_provider + return unless provider.present? + + # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is + # registered or no alert at all. In case of another alert (such as a blocked user), it is safer + # to do nothing to prevent redirection loops with certain Omniauth providers. + return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated') + + # Prevent alert from popping up on the first page shown after authentication. + flash[:alert] = nil + + redirect_to user_omniauth_authorize_path(provider.to_sym) + end + + def valid_otp_attempt?(user) + user.valid_otp?(user_params[:otp_attempt]) || + user.invalidate_otp_backup_code!(user_params[:otp_attempt]) + end + + def log_audit_event(user, options = {}) + AuditEventService.new(user, user, options). + for_authentication.security_event + end end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index cd52556b20..8e7e45c781 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,39 +1,36 @@ class SnippetsController < ApplicationController - before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow modify snippet - before_filter :authorize_modify_snippet!, only: [:edit, :update] + before_action :authorize_update_snippet!, only: [:edit, :update] # Allow destroy snippet - before_filter :authorize_admin_snippet!, only: [:destroy] + before_action :authorize_admin_snippet!, only: [:destroy] - before_filter :set_title - - skip_before_filter :authenticate_user!, only: [:index, :user_index, :show, :raw] + skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw] + layout 'snippets' respond_to :html - layout :determine_layout - def index - @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(PER_PAGE) - end + if params[:username].present? + @user = User.find_by(username: params[:username]) - def user_index - @user = User.find_by(username: params[:username]) + render_404 and return unless @user - render_404 and return unless @user + @snippets = SnippetsFinder.new.execute(current_user, { + filter: :by_user, + user: @user, + scope: params[:scope] }). + page(params[:page]).per(PER_PAGE) - @snippets = SnippetsFinder.new.execute(current_user, { - filter: :by_user, - user: @user, - scope: params[:scope] }). - page(params[:page]).per(PER_PAGE) - - if @user == current_user - render 'current_user_index' + if @user == current_user + render 'current_user_index' + else + render 'user_index' + end else - render 'user_index' + @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(PER_PAGE) end end @@ -90,24 +87,15 @@ class SnippetsController < ApplicationController end end - def authorize_modify_snippet! - return render_404 unless can?(current_user, :modify_personal_snippet, @snippet) + def authorize_update_snippet! + return render_404 unless can?(current_user, :update_personal_snippet, @snippet) end def authorize_admin_snippet! return render_404 unless can?(current_user, :admin_personal_snippet, @snippet) end - def set_title - @title = 'Snippets' - @title_url = snippets_path - end - def snippet_params params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level) end - - def determine_layout - current_user ? 'navless' : 'public_users' - end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index c5f3da54ea..28536e359e 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -1,6 +1,6 @@ class UploadsController < ApplicationController - skip_before_filter :authenticate_user! - before_filter :find_model, :authorize_access! + skip_before_action :authenticate_user! + before_action :find_model, :authorize_access! def show uploader = @model.send(upload_mount) @@ -28,7 +28,7 @@ class UploadsController < ApplicationController end def authorize_access! - authorized = + authorized = case @model when Project can?(current_user, :read_project, @model) @@ -52,13 +52,13 @@ class UploadsController < ApplicationController def upload_model upload_models = { - user: User, - project: Project, - note: Note, - group: Group + "user" => User, + "project" => Project, + "note" => Note, + "group" => Group } - upload_models[params[:model].to_sym] + upload_models[params[:model]] end def upload_mount diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 679d6897ce..2bb5c338cf 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,6 @@ class UsersController < ApplicationController - skip_before_filter :authenticate_user! - before_filter :set_user - layout :determine_layout + skip_before_action :authenticate_user! + before_action :set_user def show @contributed_projects = contributed_projects.joined(@user). @@ -13,9 +12,6 @@ class UsersController < ApplicationController # Collect only groups common for both users @groups = @user.groups & GroupsFinder.new.execute(current_user) - @title = @user.name - @title_url = user_path(@user) - respond_to do |format| format.html @@ -51,14 +47,6 @@ class UsersController < ApplicationController render 'calendar_activities', layout: false end - def determine_layout - if current_user - 'navless' - else - 'public_users' - end - end - private def set_user diff --git a/app/finders/README.md b/app/finders/README.md index 1f46518d23..1a1c69dea3 100644 --- a/app/finders/README.md +++ b/app/finders/README.md @@ -16,7 +16,7 @@ issues = project.issues_for_user_filtered_by(user, params) Better use this: ```ruby -issues = IssuesFinder.new.execute(project, user, filter) +issues = IssuesFinder.new(project, user, filter).execute ``` It will help keep models thiner. diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 2c0702073d..ab89aa2c53 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -10,7 +10,7 @@ # state: 'open' or 'closed' or 'all' # group_id: integer # project_id: integer -# milestone_id: integer +# milestone_title: string # assignee_id: integer # search: string # label_name: string @@ -23,10 +23,12 @@ class IssuableFinder attr_accessor :current_user, :params - def execute(current_user, params) + def initialize(current_user, params) @current_user = current_user @params = params + end + def execute items = init_collection items = by_scope(items) items = by_state(items) @@ -40,6 +42,77 @@ class IssuableFinder items = sort(items) end + def group + return @group if defined?(@group) + + @group = + if params[:group_id].present? + Group.find(params[:group_id]) + else + nil + end + end + + def project + return @project if defined?(@project) + + @project = + if params[:project_id].present? + Project.find(params[:project_id]) + else + nil + end + end + + def search + params[:search].presence + end + + def milestones? + params[:milestone_title].present? + end + + def milestones + return @milestones if defined?(@milestones) + + @milestones = + if milestones? && params[:milestone_title] != Milestone::None.title + Milestone.where(title: params[:milestone_title]) + else + nil + end + end + + def assignee? + params[:assignee_id].present? + end + + def assignee + return @assignee if defined?(@assignee) + + @assignee = + if assignee? && params[:assignee_id] != NONE + User.find(params[:assignee_id]) + else + nil + end + end + + def author? + params[:author_id].present? + end + + def author + return @author if defined?(@author) + + @author = + if author? && params[:author_id] != NONE + User.find(params[:author_id]) + else + nil + end + end + private def init_collection @@ -75,6 +148,8 @@ class IssuableFinder case params[:state] when 'closed' items.closed + when 'merged' + items.respond_to?(:merged) ? items.merged : items.closed when 'all' items when 'opened' @@ -85,25 +160,19 @@ class IssuableFinder end def by_group(items) - if params[:group_id].present? - items = items.of_group(Group.find(params[:group_id])) - end + items = items.of_group(group) if group items end def by_project(items) - if params[:project_id].present? - items = items.of_projects(params[:project_id]) - end + items = items.of_projects(project.id) if project items end def by_search(items) - if params[:search].present? - items = items.search(params[:search]) - end + items = items.search(search) if search items end @@ -113,24 +182,24 @@ class IssuableFinder end def by_milestone(items) - if params[:milestone_id].present? - items = items.where(milestone_id: (params[:milestone_id] == NONE ? nil : params[:milestone_id])) + if milestones? + items = items.where(milestone_id: milestones.try(:pluck, :id)) end items end def by_assignee(items) - if params[:assignee_id].present? - items = items.where(assignee_id: (params[:assignee_id] == NONE ? nil : params[:assignee_id])) + if assignee? + items = items.where(assignee_id: assignee.try(:id)) end items end def by_author(items) - if params[:author_id].present? - items = items.where(author_id: (params[:author_id] == NONE ? nil : params[:author_id])) + if author? + items = items.where(author_id: author.try(:id)) end items @@ -150,10 +219,6 @@ class IssuableFinder items end - def project - Project.where(id: params[:project_id]).first if params[:project_id].present? - end - def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index bb8d568380..14df8d4cbd 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -16,6 +16,6 @@ module AppearancesHelper end def brand_header_logo - image_tag 'logo-white.png' + image_tag 'logo.svg' end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2b41d42161..a803b66c50 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,26 +2,6 @@ require 'digest/md5' require 'uri' module ApplicationHelper - COLOR_SCHEMES = { - 1 => 'white', - 2 => 'dark', - 3 => 'solarized-light', - 4 => 'solarized-dark', - 5 => 'monokai', - } - COLOR_SCHEMES.default = 'white' - - # Helper method to access the COLOR_SCHEMES - # - # The keys are the `color_scheme_ids` - # The values are the `name` of the scheme. - # - # The preview images are `name-scheme-preview.png` - # The stylesheets should use the css class `.name` - def color_schemes - COLOR_SCHEMES.freeze - end - # Check if a particular controller is the current one # # args - One or more controller names to check @@ -138,18 +118,6 @@ module ApplicationHelper Emoji.names.to_s end - def app_theme - Gitlab::Theme.css_class_by_id(current_user.try(:theme_id)) - end - - def theme_type - Gitlab::Theme.type_css_class_by_id(current_user.try(:theme_id)) - end - - def user_color_scheme_class - COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) - end - # Define whenever show last push event # with suggestion to create MR def show_last_push_widget?(event) @@ -211,66 +179,66 @@ module ApplicationHelper BroadcastMessage.current end - def time_ago_with_tooltip(date, placement = 'top', html_class = 'time_ago') - capture_haml do - haml_tag :time, date.to_s, - class: html_class, datetime: date.getutc.iso8601, title: date.stamp('Aug 21, 2011 9:23pm'), - data: { toggle: 'tooltip', placement: placement } + # Render a `time` element with Javascript-based relative date and tooltip + # + # time - Time object + # placement - Tooltip placement String (default: "top") + # html_class - Custom class for `time` element (default: "time_ago") + # skip_js - When true, exclude the `script` tag (default: false) + # + # By default also includes a `script` element with Javascript necessary to + # initialize the `timeago` jQuery extension. If this method is called many + # times, for example rendering hundreds of commits, it's advisable to disable + # this behavior using the `skip_js` argument and re-initializing `timeago` + # manually once all of the elements have been rendered. + # + # A `js-timeago` class is always added to the element, even when a custom + # `html_class` argument is provided. + # + # Returns an HTML-safe String + def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) + element = content_tag :time, time.to_s, + class: "#{html_class} js-timeago", + datetime: time.getutc.iso8601, + title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), + data: { toggle: 'tooltip', placement: placement } - haml_tag :script, "$('." + html_class + "').timeago().tooltip()" - end.html_safe + element += javascript_tag "$('.js-timeago').timeago()" unless skip_js + + element end def render_markup(file_name, file_content) - GitHub::Markup.render(file_name, file_content). - force_encoding(file_content.encoding).html_safe + if gitlab_markdown?(file_name) + Haml::Helpers.preserve(markdown(file_content)) + elsif asciidoc?(file_name) + asciidoc(file_content) + elsif plain?(file_name) + content_tag :pre, class: 'plain-readme' do + file_content + end + else + GitHub::Markup.render(file_name, file_content). + force_encoding(file_content.encoding).html_safe + end rescue RuntimeError simple_format(file_content) end + def plain?(filename) + Gitlab::MarkupHelper.plain?(filename) + end + def markup?(filename) - Gitlab::MarkdownHelper.markup?(filename) + Gitlab::MarkupHelper.markup?(filename) end def gitlab_markdown?(filename) - Gitlab::MarkdownHelper.gitlab_markdown?(filename) + Gitlab::MarkupHelper.gitlab_markdown?(filename) end - # Overrides ActionView::Helpers::UrlHelper#link_to to add `rel="nofollow"` to - # external links - def link_to(name = nil, options = nil, html_options = {}) - if options.kind_of?(String) - if !options.start_with?('#', '/') - html_options = add_nofollow(options, html_options) - end - end - - super - end - - # Add `"rel=nofollow"` to external links - # - # link - String link to check - # html_options - Hash of `html_options` passed to `link_to` - # - # Returns `html_options`, adding `rel: nofollow` for external links - def add_nofollow(link, html_options = {}) - begin - uri = URI(link) - - if uri && uri.absolute? && uri.host != Gitlab.config.gitlab.host - rel = html_options.fetch(:rel, '') - html_options[:rel] = (rel + ' nofollow').strip - end - rescue URI::Error - # noop - end - - html_options - end - - def escaped_autolink(text) - auto_link ERB::Util.html_escape(text), link: :urls + def asciidoc?(filename) + Gitlab::MarkupHelper.asciidoc?(filename) end def promo_host @@ -319,11 +287,29 @@ module ApplicationHelper end end - def nav_sidebar_class - if nav_menu_collapsed? - "page-sidebar-collapsed" - else - "page-sidebar-expanded" + def state_filters_text_for(entity, project) + titles = { + opened: "Open" + } + + entity_title = titles[entity] || entity.to_s.humanize + + count = + if project.nil? + nil + elsif current_controller?(:issues) + project.issues.send(entity).count + elsif current_controller?(:merge_requests) + project.merge_requests.send(entity).count + end + + html = content_tag :span, entity_title + + if count.present? + html += " " + html += content_tag :span, number_with_delimiter(count), class: 'badge' end + + html.html_safe end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 241d6075c9..61d1438394 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -19,12 +19,16 @@ module ApplicationSettingsHelper current_application_settings.sign_in_text end + def user_oauth_applications? + current_application_settings.user_oauth_applications + end + # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. def restricted_level_checkboxes(help_block_id) Gitlab::VisibilityLevel.options.map do |name, level| checked = restricted_visibility_levels(true).include?(level) - css_class = 'btn btn-primary' + css_class = 'btn' css_class += ' active' if checked checkbox_name = 'application_setting[restricted_visibility_levels][]' diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb new file mode 100644 index 0000000000..0e7a37b4cc --- /dev/null +++ b/app/helpers/auth_helper.rb @@ -0,0 +1,50 @@ +module AuthHelper + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze + FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze + + def ldap_enabled? + Gitlab.config.ldap.enabled + end + + def provider_has_icon?(name) + PROVIDERS_WITH_ICONS.include?(name.to_s) + end + + def auth_providers + Gitlab::OAuth::Provider.providers + end + + def label_for_provider(name) + Gitlab::OAuth::Provider.label_for(name) + end + + def form_based_provider?(name) + FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s } + end + + def form_based_providers + auth_providers.select { |provider| form_based_provider?(provider) } + end + + def button_based_providers + auth_providers.reject { |provider| form_based_provider?(provider) } + end + + def provider_image_tag(provider, size = 64) + label = label_for_provider(provider) + + if provider_has_icon?(provider) + file_name = "#{provider.to_s.split('_').first}_#{size}.png" + + image_tag(image_path("auth_buttons/#{file_name}"), alt: label, title: "Sign in with #{label}") + else + label + end + end + + def auth_active?(provider) + current_user.identities.exists?(provider: provider.to_s) + end + + extend self +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 4ea838ca44..77d99140c4 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,6 +1,6 @@ module BlobHelper - def highlight(blob_name, blob_content, nowrap = false) - formatter = Rugments::Formatters::HTML.new( + def highlight(blob_name, blob_content, nowrap: false, continue: false) + @formatter ||= Rouge::Formatters::HTMLGitlab.new( nowrap: nowrap, cssclass: 'code highlight', lineanchors: true, @@ -8,12 +8,14 @@ module BlobHelper ) begin - lexer = Rugments::Lexer.guess(filename: blob_name, source: blob_content) - rescue Rugments::Lexer::AmbiguousGuess - lexer = Rugments::Lexers::PlainText + @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new + result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe + rescue + @lexer = Rouge::Lexers::PlainText + result = @formatter.format(@lexer.lex(blob_content)).html_safe end - formatter.format(lexer.lex(blob_content)).html_safe + result end def no_highlight_files @@ -55,7 +57,7 @@ module BlobHelper end def editing_preview_title(filename) - if Gitlab::MarkdownHelper.previewable?(filename) + if Gitlab::MarkupHelper.previewable?(filename) 'Preview' else 'Preview changes' diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 29ff47663d..6484dca6b5 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -1,9 +1,16 @@ module BroadcastMessagesHelper def broadcast_styling(broadcast_message) - if(broadcast_message.color || broadcast_message.font) - "background-color:#{broadcast_message.color};color:#{broadcast_message.font}" - else - "" + styling = '' + + if broadcast_message.color.present? + styling << "background-color: #{broadcast_message.color}" + styling << '; ' if broadcast_message.font.present? end + + if broadcast_message.font.present? + styling << "color: #{broadcast_message.font}" + end + + styling end end diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 01847c6b80..f1dc906cab 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -1,21 +1,20 @@ module CompareHelper - def compare_to_mr_button? - @project.merge_requests_enabled && - params[:from].present? && - params[:to].present? && - @repository.branch_names.include?(params[:from]) && - @repository.branch_names.include?(params[:to]) && - params[:from] != params[:to] && - !@refs_are_same + def create_mr_button?(from = params[:from], to = params[:to], project = @project) + from.present? && + to.present? && + from != to && + project.merge_requests_enabled && + project.repository.branch_names.include?(from) && + project.repository.branch_names.include?(to) end - def compare_mr_path + def create_mr_path(from = params[:from], to = params[:to], project = @project) new_namespace_project_merge_request_path( - @project.namespace, - @project, + project.namespace, + project, merge_request: { - source_branch: params[:to], - target_branch: params[:from] + source_branch: to, + target_branch: from } ) end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 4f42972a4d..1bd3ec5e0e 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -7,14 +7,23 @@ module DiffHelper end end - def safe_diff_files(diffs) - diffs.first(allowed_diff_size).map do |diff| - Gitlab::Diff::File.new(diff) + def allowed_diff_lines + if diff_hard_limit_enabled? + Commit::DIFF_HARD_LIMIT_LINES + else + Commit::DIFF_SAFE_LINES end end - def show_diff_size_warning?(diffs) - diffs.size > allowed_diff_size + def safe_diff_files(diffs) + lines = 0 + safe_files = [] + diffs.first(allowed_diff_size).each do |diff| + lines += diff.diff.lines.count + break if lines > allowed_diff_lines + safe_files << Gitlab::Diff::File.new(diff) + end + safe_files end def diff_hard_limit_enabled? @@ -92,6 +101,10 @@ module DiffHelper (bottom) ? 'js-unfold-bottom' : '' end + def unfold_class(unfold) + (unfold) ? 'unfold js-unfold' : '' + end + def diff_line_content(line) if line.blank? "  " @@ -140,8 +153,8 @@ module DiffHelper end end - def submodule_link(blob, ref) - tree, commit = submodule_links(blob, ref) + def submodule_link(blob, ref, repository = @repository) + tree, commit = submodule_links(blob, ref, repository) commit_id = if commit.nil? blob.id[0..10] else diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 0df3ecc90b..45788ba95a 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -31,8 +31,27 @@ module EmailsHelper end def color_email_diff(diffcontent) - formatter = Rugments::Formatters::HTML.new(cssclass: "highlight", inline_theme: :github) - lexer = Rugments::Lexers::Diff.new + formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github') + lexer = Rouge::Lexers::Diff raw formatter.format(lexer.lex(diffcontent)) end + + def password_reset_token_valid_time + valid_hours = Devise.reset_password_within / 60 / 60 + if valid_hours >= 24 + unit = 'day' + valid_length = (valid_hours / 24).floor + else + unit = 'hour' + valid_length = valid_hours.floor + end + + pluralize(valid_length, unit) + end + + def reset_token_expire_message + link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email)) + msg = "This link is valid for #{password_reset_token_valid_time}. " + msg << "After it expires, you can #{link_tag}." + end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index c9fd0f0362..8428281f8f 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -25,12 +25,16 @@ module EventsHelper def event_filter_link(key, tooltip) key = key.to_s - active = if @event_filter.active? key - 'active' - end + active = 'active' if @event_filter.active?(key) + link_opts = { + class: 'event_filter_link', + id: "#{key}_event_filter", + title: "Filter by #{tooltip.downcase}", + data: { toggle: 'tooltip', placement: 'top' } + } content_tag :li, class: "filter_icon #{active}" do - link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => 'Filter by ' + tooltip.downcase do + link_to request.path, link_opts do icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip) end end @@ -164,8 +168,8 @@ module EventsHelper end end - def event_note(text) - text = first_line_in_markdown(text, 150) + def event_note(text, options = {}) + text = first_line_in_markdown(text, 150, options) sanitize(text, tags: %w(a img b pre code p span)) end @@ -185,7 +189,7 @@ module EventsHelper xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" xml.link href: event_link xml.title truncate(event_title, length: 80) - xml.updated event.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") + xml.updated event.created_at.xmlschema xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email) xml.author do |author| xml.name event.author_name diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index aa1de2f50e..eb3f72a307 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -1,5 +1,8 @@ +require 'nokogiri' + module GitlabMarkdownHelper include Gitlab::Markdown + include PreferencesHelper # Use this in places where you would normally use link_to(gfm(...), ...). # @@ -19,198 +22,103 @@ module GitlabMarkdownHelper escape_once(body) end - gfm_body = gfm(escaped_body, @project, html_options) + gfm_body = gfm(escaped_body, {}, html_options) - gfm_body.gsub!(%r{.*?}m) do |match| - "#{match}#{link_to("", url, html_options)[0..-5]}" # "".length +1 + fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body) + if fragment.children.size == 1 && fragment.children[0].name == 'a' + # Fragment has only one node, and it's a link generated by `gfm`. + # Replace it with our requested link. + text = fragment.children[0].text + fragment.children[0].replace(link_to(text, url, html_options)) + else + # Traverse the fragment's first generation of children looking for pure + # text, wrapping anything found in the requested link + fragment.children.each do |node| + next unless node.text? + node.replace(link_to(node.text, url, html_options)) + end end - link_to(gfm_body.html_safe, url, html_options) + fragment.to_html.html_safe end + MARKDOWN_OPTIONS = { + no_intra_emphasis: true, + tables: true, + fenced_code_blocks: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true, + footnotes: true + }.freeze + def markdown(text, options={}) unless @markdown && options == @options @options = options # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch - rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, { - with_toc_data: true, - safe_links_only: true, - # Handled further down the line by HTML::Pipeline::SanitizationFilter - escape_html: false - }.merge(options)) + rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, options) # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use - @markdown = Redcarpet::Markdown.new(rend, - no_intra_emphasis: true, - tables: true, - fenced_code_blocks: true, - autolink: true, - strikethrough: true, - lax_spacing: true, - space_after_headers: true, - superscript: true - ) + @markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS) end @markdown.render(text).html_safe end + def asciidoc(text) + Gitlab::Asciidoc.render(text, { + commit: @commit, + project: @project, + project_wiki: @project_wiki, + requested_path: @path, + ref: @ref + }) + end + # Return the first line of +text+, up to +max_chars+, after parsing the line # as Markdown. HTML tags in the parsed output are not counted toward the # +max_chars+ limit. If the length limit falls within a tag's contents, then # the tag contents are truncated without removing the closing tag. - def first_line_in_markdown(text, max_chars = nil) - md = markdown(text).strip + def first_line_in_markdown(text, max_chars = nil, options = {}) + md = markdown(text, options).strip truncate_visible(md, max_chars || md.length) if md.present? end def render_wiki_content(wiki_page) - if wiki_page.format == :markdown + case wiki_page.format + when :markdown markdown(wiki_page.content) + when :asciidoc + asciidoc(wiki_page.content) else wiki_page.formatted_content.html_safe end end - def create_relative_links(text) - paths = extract_paths(text) + MARKDOWN_TIPS = [ + "End a line with two or more spaces for a line-break, or soft-return", + "Inline code can be denoted by `surrounding it with backticks`", + "Blocks of code can be denoted by three backticks ``` or four leading spaces", + "Emoji can be added by :emoji_name:, for example :thumbsup:", + "Notify other participants using @user_name", + "Notify a specific group using @group_name", + "Notify the entire team using @all", + "Reference an issue using a hash, for example issue #123", + "Reference a merge request using an exclamation point, for example MR !123", + "Italicize words or phrases using *asterisks* or _underscores_", + "Bold words or phrases using **double asterisks** or __double underscores__", + "Strikethrough words or phrases using ~~two tildes~~", + "Make a bulleted list using + pluses, - minuses, or * asterisks", + "Denote blockquotes using > at the beginning of a line", + "Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___" + ].freeze - paths.uniq.each do |file_path| - # If project does not have repository - # its nothing to rebuild - # - # TODO: pass project variable to markdown helper instead of using - # instance variable. Right now it generates invalid path for pages out - # of project scope. Example: search results where can be rendered markdown - # from different projects - if @repository && @repository.exists? && !@repository.empty? - new_path = rebuild_path(file_path) - # Finds quoted path so we don't replace other mentions of the string - # eg. "doc/api" will be replaced and "/home/doc/api/text" won't - text.gsub!("\"#{file_path}\"", "\"/#{new_path}\"") - end - end - - text - end - - def extract_paths(text) - links = substitute_links(text) - image_links = substitute_image_links(text) - links + image_links - end - - def substitute_links(text) - links = text.scan(//) - relative_links = links.flatten.reject{ |link| link_to_ignore? link } - relative_links - end - - def substitute_image_links(text) - links = text.scan(/ - true - else - ignored_protocols.map{ |protocol| link.include?(protocol) }.any? - end - end - - def ignored_protocols - ["http://","https://", "ftp://", "mailto:", "smb://"] - end - - def rebuild_path(file_path) - file_path = file_path.dup - file_path.gsub!(/(#.*)/, "") - id = $1 || "" - file_path = relative_file_path(file_path) - file_path = sanitize_slashes(file_path) - - [ - Gitlab.config.gitlab.relative_url_root, - @project.path_with_namespace, - path_with_ref(file_path), - file_path - ].compact.join("/").gsub(/\A\/*|\/*\z/, '') + id - end - - def sanitize_slashes(path) - path[0] = "" if path.start_with?("/") - path.chop if path.end_with?("/") - path - end - - def relative_file_path(path) - requested_path = @path - nested_path = build_nested_path(path, requested_path) - return nested_path if file_exists?(nested_path) - path - end - - # Covering a special case, when the link is referencing file in the same directory eg: - # If we are at doc/api/README.md and the README.md contains relative links like [Users](users.md) - # this takes the request path(doc/api/README.md), and replaces the README.md with users.md so the path looks like doc/api/users.md - # If we are at doc/api and the README.md shown in below the tree view - # this takes the request path(doc/api) and adds users.md so the path looks like doc/api/users.md - def build_nested_path(path, request_path) - return request_path if path == "" - return path unless request_path - if local_path(request_path) == "tree" - base = request_path.split("/").push(path) - base.join("/") - else - base = request_path.split("/") - base.pop - base.push(path).join("/") - end - end - - # Checks if the path exists in the repo - # eg. checks if doc/README.md exists, if not then link to blob - def path_with_ref(path) - if file_exists?(path) - "#{local_path(path)}/#{correct_ref}" - else - "blob/#{correct_ref}" - end - end - - def file_exists?(path) - return false if path.nil? - @repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any? - end - - # Check if the path is pointing to a directory(tree) or a file(blob) - # eg. doc/api is directory and doc/README.md is file - def local_path(path) - return "tree" if @repository.tree(current_sha, path).entries.any? - return "raw" if @repository.blob_at(current_sha, path).image? - "blob" - end - - def current_sha - if @commit - @commit.id - elsif @repository && !@repository.empty? - if @ref - @repository.commit(@ref).try(:sha) - else - @repository.head_commit.sha - end - end - end - - # We will assume that if no ref exists we can point to master - def correct_ref - @ref ? @ref : "master" + # Returns a random markdown tip for use as a textarea placeholder + def random_markdown_tip + MARKDOWN_TIPS.sample end private @@ -261,15 +169,25 @@ module GitlabMarkdownHelper end end + # Returns the text necessary to reference `entity` across projects + # + # project - Project to reference + # entity - Object that responds to `to_reference` + # + # Examples: + # + # cross_project_reference(project, project.issues.first) + # # => 'namespace1/project1#123' + # + # cross_project_reference(project, project.merge_requests.first) + # # => 'namespace1/project1!345' + # + # Returns a String def cross_project_reference(project, entity) - path = project.path_with_namespace - - if entity.kind_of?(Issue) - [path, entity.iid].join('#') - elsif entity.kind_of?(MergeRequest) - [path, entity.iid].join('!') + if entity.respond_to?(:to_reference) + "#{project.to_reference}#{entity.to_reference}" else - raise 'Not supported type' + '' end end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 9703c8d9e9..d0fae255a0 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -17,6 +17,10 @@ module GitlabRoutingHelper namespace_project_path(project.namespace, project, *args) end + def activity_project_path(project, *args) + activity_namespace_project_path(project.namespace, project, *args) + end + def edit_project_path(project, *args) edit_namespace_project_path(project.namespace, project, *args) end @@ -52,4 +56,12 @@ module GitlabRoutingHelper def project_snippet_url(entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) end + + def toggle_subscription_path(entity, *args) + if entity.is_a?(Issue) + toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) + else + toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity) + end + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index add0a776a6..b067cb54a4 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -19,32 +19,6 @@ module GroupsHelper end end - def group_head_title - title = @group.name - - title = if current_action?(:issues) - "Issues - " + title - elsif current_action?(:merge_requests) - "Merge requests - " + title - elsif current_action?(:members) - "Members - " + title - elsif current_action?(:edit) - "Settings - " + title - else - title - end - - title - end - - def group_settings_page? - if current_controller?('groups') - current_action?('edit') || current_action?('projects') - else - false - end - end - def group_icon(group) if group.is_a?(String) group = Group.find_by(path: group) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index a9030729b4..30b17a736a 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -1,4 +1,6 @@ module IconsHelper + include FontAwesome::Rails::IconHelper + # Creates an icon tag given icon name(s) and possible icon modifiers. # # Right now this method simply delegates directly to `fa_icon` from the @@ -26,15 +28,15 @@ module IconsHelper end def public_icon - icon('globe') + icon('globe fw') end def internal_icon - icon('shield') + icon('shield fw') end def private_icon - icon('lock') + icon('lock fw') end def file_type_icon_class(type, mode, name) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index ad4a761272..6ddb37cd0d 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -43,32 +43,6 @@ module IssuesHelper end end - def title_for_issue(issue_iid, project = @project) - return '' if project.nil? - - if project.default_issues_tracker? - issue = project.issues.where(iid: issue_iid).first - return issue.title if issue - end - - '' - end - - def issue_timestamp(issue) - # Shows the created at time and the updated at time if different - ts = "#{time_ago_with_tooltip(issue.created_at, 'bottom', 'note_created_ago')}" - if issue.updated_at != issue.created_at - ts << capture_haml do - haml_tag :span do - haml_concat '·' - haml_concat icon('edit', title: 'edited') - haml_concat time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_edited_ago') - end - end - end - ts.html_safe - end - def bulk_update_milestone_options options_for_select([['None (backlog)', -1]]) + options_from_collection_for_select(project_active_milestones, 'id', @@ -108,4 +82,7 @@ module IssuesHelper xml.summary issue.title end end + + # Required for Gitlab::Markdown::IssueReferenceFilter + module_function :url_for_issue end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 32ef2e7ca8..8036303851 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,4 +1,44 @@ module LabelsHelper + include ActionView::Helpers::TagHelper + + # Link to a Label + # + # label - Label object to link to + # project - Project object which will be used as the context for the label's + # link. If omitted, defaults to `@project`, or the label's own + # project. + # block - An optional block that will be passed to `link_to`, forming the + # body of the link element. If omitted, defaults to + # `render_colored_label`. + # + # Examples: + # + # # Allow the generated link to use the label's own project + # link_to_label(label) + # + # # Force the generated link to use @project + # @project = Project.first + # link_to_label(label) + # + # # Force the generated link to use a provided project + # link_to_label(label, project: Project.last) + # + # # Customize link body with a block + # link_to_label(label) { "My Custom Label Text" } + # + # Returns a String + def link_to_label(label, project: nil, &block) + project ||= @project || label.project + link = namespace_project_issues_path(project.namespace, project, + label_name: label.name) + + if block_given? + link_to link, &block + else + link_to render_colored_label(label), link + end + end + def project_label_names @project.labels.pluck(:title) end @@ -7,9 +47,13 @@ module LabelsHelper label_color = label.color || Label::DEFAULT_COLOR text_color = text_color_for_bg(label_color) - content_tag :span, class: 'label color-label', style: "background-color:#{label_color};color:#{text_color}" do - label.name - end + # Intentionally not using content_tag here so that this method can be called + # by LabelReferenceFilter + span = %() + + escape_once(label.name) + '' + + span.html_safe end def suggested_colors @@ -42,13 +86,16 @@ module LabelsHelper r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex) if (r + g + b) > 500 - "#333" + '#333333' else - "#FFF" + '#FFFFFF' end end def project_labels_options(project) options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) end + + # Required for Gitlab::Markdown::LabelReferenceFilter + module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 54462fd00e..f8169b4f28 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -49,4 +49,26 @@ module MergeRequestsHelper def issues_sentence(issues) issues.map { |i| "##{i.iid}" }.to_sentence end + + def mr_change_branches_path(merge_request) + new_namespace_project_merge_request_path( + @project.namespace, @project, + merge_request: { + source_project_id: @merge_request.source_project_id, + target_project_id: @merge_request.target_project_id, + source_branch: @merge_request.source_branch, + target_branch: nil + } + ) + end + + def source_branch_with_namespace(merge_request) + if merge_request.for_fork? + namespace = link_to(merge_request.source_project_namespace, + project_path(merge_request.source_project)) + namespace + ":#{merge_request.source_branch}" + else + merge_request.source_branch + end + end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 282bdf744d..132a893e53 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -28,6 +28,9 @@ module MilestonesHelper Milestone.where(project_id: @projects) end.active - options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) + grouped_milestones = Milestones::GroupService.new(milestones).execute + grouped_milestones.unshift(Milestone::None) + + options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title]) end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 2b03269800..9b1dd8b8e5 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -2,4 +2,20 @@ module NavHelper def nav_menu_collapsed? cookies[:collapsed_nav] == 'true' end + + def nav_sidebar_class + if nav_menu_collapsed? + "page-sidebar-collapsed" + else + "page-sidebar-expanded" + end + end + + def nav_header_class + if nav_menu_collapsed? + "header-collapsed" + else + "header-expanded" + end + end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index ab44fa6ee4..5f0c921413 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -9,6 +9,10 @@ module NotesHelper hidden_field_tag(:target_id, note.noteable.id) end + def note_editable?(note) + note.editable? && can?(current_user, :admin_note, note) + end + def link_to_commit_diff_line_note(note) if note.for_commit_diff_line? link_to( @@ -19,21 +23,6 @@ module NotesHelper end end - def note_timestamp(note) - # Shows the created at time and the updated at time if different - ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')}" - if note.updated_at != note.created_at - ts << capture_haml do - haml_tag :span do - haml_concat '·' - haml_concat icon('edit', title: 'edited') - haml_concat time_ago_with_tooltip(note.updated_at, 'bottom', 'note_edited_ago') - end - end - end - ts.html_safe - end - def noteable_json(noteable) { id: noteable.id, @@ -43,7 +32,7 @@ module NotesHelper }.to_json end - def link_to_new_diff_note(line_code) + def link_to_new_diff_note(line_code, line_type = nil) discussion_id = Note.build_discussion_id( @comments_target[:noteable_type], @comments_target[:noteable_id] || @comments_target[:commit_id], @@ -55,7 +44,8 @@ module NotesHelper noteable_id: @comments_target[:noteable_id], commit_id: @comments_target[:commit_id], line_code: line_code, - discussion_id: discussion_id + discussion_id: discussion_id, + line_type: line_type } button_tag(class: 'btn add-diff-note js-add-diff-note-button', @@ -65,7 +55,7 @@ module NotesHelper end end - def link_to_reply_diff(note) + def link_to_reply_diff(note, line_type = nil) return unless current_user data = { @@ -73,7 +63,8 @@ module NotesHelper noteable_id: note.noteable_id, commit_id: note.commit_id, line_code: note.line_code, - discussion_id: note.discussion_id + discussion_id: note.discussion_id, + line_type: line_type } button_tag class: 'btn reply-btn js-discussion-reply-button', diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index f771fe761e..2f8e64c375 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,4 +1,6 @@ module NotificationsHelper + include IconsHelper + def notification_icon(notification) if notification.disabled? icon('volume-off', class: 'ns-mute') diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb deleted file mode 100644 index 997b91de07..0000000000 --- a/app/helpers/oauth_helper.rb +++ /dev/null @@ -1,34 +0,0 @@ -module OauthHelper - def ldap_enabled? - Gitlab.config.ldap.enabled - end - - def default_providers - [:twitter, :github, :gitlab, :bitbucket, :google_oauth2, :ldap] - end - - def enabled_oauth_providers - Devise.omniauth_providers - end - - def enabled_social_providers - enabled_oauth_providers.select do |name| - [:twitter, :gitlab, :github, :bitbucket, :google_oauth2].include?(name.to_sym) - end - end - - def additional_providers - enabled_oauth_providers.reject{|provider| provider.to_s.starts_with?('ldap')} - end - - def oauth_image_tag(provider, size = 64) - file_name = "#{provider.to_s.split('_').first}_#{size}.png" - image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}") - end - - def oauth_active?(provider) - current_user.identities.exists?(provider: provider.to_s) - end - - extend self -end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb new file mode 100644 index 0000000000..01b6a63552 --- /dev/null +++ b/app/helpers/page_layout_helper.rb @@ -0,0 +1,26 @@ +module PageLayoutHelper + def page_title(*titles) + @page_title ||= [] + + @page_title.push(*titles.compact) if titles.any? + + @page_title.join(" | ") + end + + def header_title(title = nil, title_url = nil) + if title + @header_title = title + @header_title_url = title_url + else + @header_title_url ? link_to(@header_title, @header_title_url) : @header_title + end + end + + def sidebar(name = nil) + if name + @sidebar = name + else + @sidebar + end + end +end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb new file mode 100644 index 0000000000..ea774e28ec --- /dev/null +++ b/app/helpers/preferences_helper.rb @@ -0,0 +1,65 @@ +# Helper methods for per-User preferences +module PreferencesHelper + COLOR_SCHEMES = { + 1 => 'white', + 2 => 'dark', + 3 => 'solarized-light', + 4 => 'solarized-dark', + 5 => 'monokai', + } + COLOR_SCHEMES.default = 'white' + + # Helper method to access the COLOR_SCHEMES + # + # The keys are the `color_scheme_ids` + # The values are the `name` of the scheme. + # + # The preview images are `name-scheme-preview.png` + # The stylesheets should use the css class `.name` + def color_schemes + COLOR_SCHEMES.freeze + end + + # Maps `dashboard` values to more user-friendly option text + DASHBOARD_CHOICES = { + projects: 'Your Projects (default)', + stars: 'Starred Projects' + }.with_indifferent_access.freeze + + # Returns an Array usable by a select field for more user-friendly option text + def dashboard_choices + defined = User.dashboards + + if defined.size != DASHBOARD_CHOICES.size + # Ensure that anyone adding new options updates this method too + raise RuntimeError, "`User` defines #{defined.size} dashboard choices," + + " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." + else + defined.map do |key, _| + # Use `fetch` so `KeyError` gets raised when a key is missing + [DASHBOARD_CHOICES.fetch(key), key] + end + end + end + + def project_view_choices + [ + ['Readme (default)', :readme], + ['Activity view', :activity] + ] + end + + def user_application_theme + theme = Gitlab::Themes.by_id(current_user.try(:theme_id)) + theme.css_class + end + + def user_color_scheme_class + COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) + end + + def prefer_readme? + !current_user || + current_user.project_view == 'readme' + end +end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb deleted file mode 100644 index 780c7cd513..0000000000 --- a/app/helpers/profile_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ProfileHelper - def show_profile_username_tab? - current_user.can_change_username? - end - - def show_profile_social_tab? - enabled_social_providers.any? - end - - def show_profile_remove_tab? - signup_enabled? - end -end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c2a7732e6f..ab9b068de0 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -21,7 +21,7 @@ module ProjectsHelper end def link_to_member(project, author, opts = {}) - default_opts = { avatar: true, name: true, size: 16 } + default_opts = { avatar: true, name: true, size: 16, author_class: 'author' } opts = default_opts.merge(opts) return "(deleted)" unless author @@ -32,7 +32,7 @@ module ProjectsHelper author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] # Build name span tag - author_html << content_tag(:span, sanitize(author.name), class: 'author') if opts[:name] + author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] author_html = author_html.html_safe @@ -84,53 +84,6 @@ module ProjectsHelper @project.milestones.active.order("due_date, title ASC") end - def link_to_toggle_star(title, starred) - cls = 'star-btn btn btn-sm btn-default' - - toggle_text = - if starred - ' Unstar' - else - ' Star' - end - - toggle_html = content_tag('span', class: 'toggle') do - icon('star') + toggle_text - end - - count_html = content_tag('span', class: 'count') do - @project.star_count.to_s - end - - link_opts = { - title: title, - class: cls, - method: :post, - remote: true, - data: { type: 'json' } - } - - path = toggle_star_namespace_project_path(@project.namespace, @project) - - content_tag 'span', class: starred ? 'turn-on' : 'turn-off' do - link_to(path, link_opts) do - toggle_html + ' ' + count_html - end - end - end - - def link_to_toggle_fork - html = content_tag('span') do - icon('code-fork') + ' Fork' - end - - count_html = content_tag(:span, class: 'count') do - @project.forks_count.to_s - end - - html + count_html - end - def project_for_deploy_key(deploy_key) if deploy_key.projects.include?(@project) @project @@ -139,6 +92,16 @@ module ProjectsHelper end end + def can_change_visibility_level?(project, current_user) + return false unless can?(current_user, :change_visibility_level, project) + + if project.forked? + project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE + else + true + end + end + private def get_project_nav_tabs(project, current_user) @@ -148,7 +111,7 @@ module ProjectsHelper nav_tabs << [:files, :commits, :network, :graphs] end - if project.repo_exists? && project.merge_requests_enabled + if project.repo_exists? && can?(current_user, :read_merge_request, project) nav_tabs << :merge_requests end @@ -156,12 +119,24 @@ module ProjectsHelper nav_tabs << :settings end - [:issues, :wiki, :snippets].each do |feature| - nav_tabs << feature if project.send :"#{feature}_enabled" + if can?(current_user, :read_issue, project) + nav_tabs << :issues end - if project.issues_enabled || project.merge_requests_enabled - nav_tabs << [:milestones, :labels] + if can?(current_user, :read_wiki, project) + nav_tabs << :wiki + end + + if can?(current_user, :read_project_snippet, project) + nav_tabs << :snippets + end + + if can?(current_user, :read_label, project) + nav_tabs << :labels + end + + if can?(current_user, :read_milestone, project) + nav_tabs << :milestones end nav_tabs.flatten @@ -192,46 +167,6 @@ module ProjectsHelper 'unknown' end - def project_head_title - title = @project.name_with_namespace - - title = if current_controller?(:tree) - "#{@project.path}\/#{@path} at #{@ref} - " + title - elsif current_controller?(:issues) - if current_action?(:show) - "Issue ##{@issue.iid} - #{@issue.title} - " + title - else - "Issues - " + title - end - elsif current_controller?(:blob) - if current_action?(:new) || current_action?(:create) - "New file at #{@ref}" - elsif current_action?(:show) - "#{@blob.path} at #{@ref}" - elsif @blob - "Edit file #{@blob.path} at #{@ref}" - end - elsif current_controller?(:commits) - "Commits at #{@ref} - " + title - elsif current_controller?(:merge_requests) - if current_action?(:show) - "Merge request ##{@merge_request.iid} - " + title - else - "Merge requests - " + title - end - elsif current_controller?(:wikis) - "Wiki - " + title - elsif current_controller?(:network) - "Network graph - " + title - elsif current_controller?(:graphs) - "Graphs - " + title - else - title - end - - title - end - def default_url_to_repo(project = nil) project = project || @project current_user ? project.url_to_repo : project.http_url_to_repo @@ -243,13 +178,49 @@ module ProjectsHelper def project_last_activity(project) if project.last_activity_at - time_ago_with_tooltip(project.last_activity_at, 'bottom', 'last_activity_time_ago') + time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago') else "Never" end end - def contribution_guide_url(project) + def add_contribution_guide_path(project) + if project && !project.repository.contribution_guide + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "CONTRIBUTING.md", + commit_message: "Add contribution guide" + ) + end + end + + def add_changelog_path(project) + if project && !project.repository.changelog + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "CHANGELOG", + commit_message: "Add changelog" + ) + end + end + + def add_license_path(project) + if project && !project.repository.license + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "LICENSE", + commit_message: "Add license" + ) + end + end + + def contribution_guide_path(project) if project && contribution_guide = project.repository.contribution_guide namespace_project_blob_path( project.namespace, @@ -260,37 +231,20 @@ module ProjectsHelper end end - def changelog_url(project) - if project && changelog = project.repository.changelog - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - changelog.name) - ) - end + def readme_path(project) + filename_path(project, :readme) end - def license_url(project) - if project && license = project.repository.license - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - license.name) - ) - end + def changelog_path(project) + filename_path(project, :changelog) end - def version_url(project) - if project && version = project.repository.version - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - version.name) - ) - end + def license_path(project) + filename_path(project, :license) + end + + def version_path(project) + filename_path(project, :version) end def hidden_pass_url(original_url) @@ -317,13 +271,60 @@ module ProjectsHelper end end - def service_field_value(type, value) - return value unless type == 'password' + def user_max_access_in_project(user, project) + level = project.team.max_member_access(user) - if value.present? - "***********" + if level + Gitlab::Access.options_with_owner.key(level) + end + end + + def leave_project_message(project) + "Are you sure you want to leave \"#{project.name}\" project?" + end + + def new_readme_path + ref = @repository.root_ref if @repository + ref ||= 'master' + + namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') + end + + def last_push_event + if current_user + current_user.recent_push(@project.id) + end + end + + def readme_cache_key + sha = @project.commit.try(:sha) || 'nil' + [@project.id, sha, "readme"].join('-') + end + + def round_commit_count(project) + count = project.commit_count + + if count > 10000 + '10000+' + elsif count > 5000 + '5000+' + elsif count > 1000 + '1000+' else - nil + count + end + end + + private + + def filename_path(project, filename) + if project && blob = project.repository.send(filename) + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + blob.name) + ) end end end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index bec8f2f1aa..12fce8db70 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -10,6 +10,8 @@ module SelectsHelper any_user = opts[:any_user] || false email_user = opts[:email_user] || false first_user = opts[:first_user] && current_user ? current_user.username : false + current_user = opts[:current_user] || false + project = opts[:project] || @project html = { class: css_class, @@ -17,12 +19,13 @@ module SelectsHelper 'data-null-user' => null_user, 'data-any-user' => any_user, 'data-email-user' => email_user, - 'data-first-user' => first_user + 'data-first-user' => first_user, + 'data-current-user' => current_user } unless opts[:scope] == :all - if @project - html['data-project-id'] = @project.id + if project + html['data-project-id'] = project.id elsif @group html['data-group-id'] = @group.id end diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 9954617c76..b3f50ceebe 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -2,8 +2,8 @@ module SubmoduleHelper include Gitlab::ShellAdapter # links to files listing for submodule if submodule is a project on this server - def submodule_links(submodule_item, ref = nil) - url = @repository.submodule_url_for(ref, submodule_item.path) + def submodule_links(submodule_item, ref = nil, repository = @repository) + url = repository.submodule_url_for(ref, submodule_item.path) return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/ @@ -63,7 +63,7 @@ module SubmoduleHelper namespace = components.pop.gsub(/^\.\.$/, '') if namespace.empty? - namespace = @project.group.path + namespace = @project.namespace.path end [ diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index a1d263d9d3..77727337f0 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -89,7 +89,7 @@ module TabHelper def project_tab_class return "active" if current_page?(controller: "/projects", action: :edit, id: @project) - if ['services', 'hooks', 'deploy_keys', 'project_members', 'protected_branches'].include? controller.controller_name + if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name "active" end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 6dd9b6f017..03a49e119b 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -25,13 +25,7 @@ module TreeHelper end def render_readme(readme) - if gitlab_markdown?(readme.name) - preserve(markdown(readme.data)) - elsif markup?(readme.name) - render_markup(readme.name, readme.data) - else - simple_format(readme.data) - end + render_markup(readme.name, readme.data) end # Return an image icon depending on the file type and mode diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb new file mode 100644 index 0000000000..f64d730b44 --- /dev/null +++ b/app/helpers/version_check_helper.rb @@ -0,0 +1,7 @@ +module VersionCheckHelper + def version_status_badge + if Rails.env.production? + image_tag VersionCheck.new.url + end + end +end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 0d573e72a8..b52cd23aba 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -10,7 +10,21 @@ module VisibilityLevelHelper end end - def visibility_level_description(level) + # Return the description for the +level+ argument. + # + # +level+ One of the Gitlab::VisibilityLevel constants + # +form_model+ Either a model object (Project, Snippet, etc.) or the name of + # a Project or Snippet class. + def visibility_level_description(level, form_model) + case form_model.is_a?(String) ? form_model : form_model.class.name + when 'PersonalSnippet', 'ProjectSnippet', 'Snippet' + snippet_visibility_level_description(level) + when 'Project' + project_visibility_level_description(level) + end + end + + def project_visibility_level_description(level) capture_haml do haml_tag :span do case level @@ -33,7 +47,7 @@ module VisibilityLevelHelper haml_tag :span do case level when Gitlab::VisibilityLevel::PRIVATE - haml_concat "The snippet is visible only for me" + haml_concat "The snippet is visible only for me." when Gitlab::VisibilityLevel::INTERNAL haml_concat "The snippet is visible for any logged in user." when Gitlab::VisibilityLevel::PUBLIC @@ -64,4 +78,18 @@ module VisibilityLevelHelper return [] if current_user.is_admin? && !show_all current_application_settings.restricted_visibility_levels || [] end + + def default_project_visibility + current_application_settings.default_project_visibility + end + + def default_snippet_visibility + current_application_settings.default_snippet_visibility + end + + def skip_level?(form_model, level) + form_model.is_a?(Project) && + form_model.forked? && + !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level) + end end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index a3bc64c010..f8a96516e6 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -6,6 +6,8 @@ module WikiHelper case wiki_page when Symbol wiki_page + when String + wiki_page else wiki_page.slug end diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb new file mode 100644 index 0000000000..b616add283 --- /dev/null +++ b/app/mailers/devise_mailer.rb @@ -0,0 +1,4 @@ +class DeviseMailer < Devise::Mailer + default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" + default reply_to: Gitlab.config.gitlab.email_reply_to +end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 0dbb2939bb..4a6e18e6a7 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -79,7 +79,7 @@ module Emails @disable_diffs = disable_diffs if @compare - @commits = Commit.decorate(compare.commits) + @commits = Commit.decorate(compare.commits, @project) @diffs = compare.diffs end @@ -93,7 +93,8 @@ module Emails "pushed to" end - @subject = "[#{@project.path_with_namespace}]" + @subject = "[Git]" + @subject << "[#{@project.path_with_namespace}]" @subject << "[#{@ref_name}]" if action == :push @subject << " " @@ -101,8 +102,8 @@ module Emails if @commits.length > 1 @target_url = namespace_project_compare_url(@project.namespace, @project, - from: Commit.new(@compare.base), - to: Commit.new(@compare.head)) + from: Commit.new(@compare.base, @project), + to: Commit.new(@compare.head, @project)) @subject << "Deleted " if @reverse_compare @subject << "#{@commits.length} commits: #{@commits.first.title}" else diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 2c0d451511..79fb48b00d 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -16,11 +16,6 @@ class Notify < ActionMailer::Base attr_accessor :current_user helper_method :current_user, :can? - default_url_options[:host] = Gitlab.config.gitlab.host - default_url_options[:protocol] = Gitlab.config.gitlab.protocol - default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? - default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root - default from: Proc.new { default_sender_address.format } default reply_to: Gitlab.config.gitlab.email_reply_to @@ -69,7 +64,7 @@ class Notify < ActionMailer::Base # Only the displayed name changes; the actual email address is always the same. def sender(sender_id, send_from_user_email = false) return unless sender = User.find(sender_id) - + address = default_sender_address address.display_name = sender.name diff --git a/app/models/ability.rb b/app/models/ability.rb index 85a15596f8..f8e5afa9b0 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -31,10 +31,11 @@ class Ability end if project && project.public? - [ + rules = [ :read_project, :read_wiki, :read_issue, + :read_label, :read_milestone, :read_project_snippet, :read_project_member, @@ -42,6 +43,8 @@ class Ability :read_note, :download_code ] + + rules - project_disabled_features_rules(project) else group = if subject.kind_of?(Group) subject @@ -68,6 +71,7 @@ class Ability def project_abilities(user, project) rules = [] key = "/user/#{user.id}/project/#{project.id}" + RequestStore.store[key] ||= begin team = project.team @@ -101,7 +105,7 @@ class Ability rules -= project_archived_rules end - rules + rules - project_disabled_features_rules(project) end end @@ -117,14 +121,15 @@ class Ability :read_project, :read_wiki, :read_issue, + :read_label, :read_milestone, :read_project_snippet, :read_project_member, :read_merge_request, :read_note, - :write_project, - :write_issue, - :write_note + :create_project, + :create_issue, + :create_note ] end @@ -132,27 +137,28 @@ class Ability project_guest_rules + [ :download_code, :fork_project, - :write_project_snippet + :create_project_snippet, + :update_issue, + :admin_issue, + :admin_label ] end def project_dev_rules project_report_rules + [ - :write_merge_request, - :write_wiki, - :modify_issue, - :admin_issue, - :admin_label, + :admin_merge_request, + :create_merge_request, + :create_wiki, :push_code ] end def project_archived_rules [ - :write_merge_request, + :create_merge_request, :push_code, :push_code_to_protected_branches, - :modify_merge_request, + :update_merge_request, :admin_merge_request ] end @@ -160,10 +166,8 @@ class Ability def project_master_rules project_dev_rules + [ :push_code_to_protected_branches, - :modify_issue, - :modify_project_snippet, - :modify_merge_request, - :admin_issue, + :update_project_snippet, + :update_merge_request, :admin_milestone, :admin_project_snippet, :admin_project_member, @@ -184,6 +188,33 @@ class Ability ] end + def project_disabled_features_rules(project) + rules = [] + + unless project.issues_enabled + rules += named_abilities('issue') + end + + unless project.merge_requests_enabled + rules += named_abilities('merge_request') + end + + unless project.issues_enabled or project.merge_requests_enabled + rules += named_abilities('label') + rules += named_abilities('milestone') + end + + unless project.snippets_enabled + rules += named_abilities('project_snippet') + end + + unless project.wiki_enabled + rules += named_abilities('wiki') + end + + rules + end + def group_abilities(user, group) rules = [] @@ -202,7 +233,8 @@ class Ability if group.has_owner?(user) || user.admin? rules.push(*[ :admin_group, - :admin_namespace + :admin_namespace, + :admin_group_member ]) end @@ -223,30 +255,40 @@ class Ability rules.flatten end - [:issue, :note, :project_snippet, :personal_snippet, :merge_request].each do |name| + + [:issue, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| - if subject.author == user || user.is_admin? - rules = [ + rules = [] + + if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user) + rules += [ :"read_#{name}", - :"write_#{name}", - :"modify_#{name}", + :"update_#{name}", + ] + end + + rules += project_abilities(user, subject.project) + rules + end + end + + [:note, :project_snippet, :personal_snippet].each do |name| + define_method "#{name}_abilities" do |user, subject| + rules = [] + + if subject.author == user + rules += [ + :"read_#{name}", + :"update_#{name}", :"admin_#{name}" ] - rules.push(:change_visibility_level) if subject.is_a?(Snippet) - rules - elsif subject.respond_to?(:assignee) && subject.assignee == user - [ - :"read_#{name}", - :"write_#{name}", - :"modify_#{name}", - ] - else - if subject.respond_to?(:project) - project_abilities(user, subject.project) - else - [] - end end + + if subject.respond_to?(:project) && subject.project + rules += project_abilities(user, subject.project) + end + + rules end end @@ -254,14 +296,17 @@ class Ability rules = [] target_user = subject.user group = subject.group - can_manage = group_abilities(user, group).include?(:admin_group) + can_manage = group_abilities(user, group).include?(:admin_group_member) + if can_manage && (user != target_user) - rules << :modify_group_member + rules << :update_group_member rules << :destroy_group_member end + if !group.last_owner?(user) && (can_manage || (user == target_user)) rules << :destroy_group_member end + rules end @@ -272,5 +317,16 @@ class Ability abilities end end + + private + + def named_abilities(name) + [ + :"read_#{name}", + :"create_#{name}", + :"update_#{name}", + :"admin_#{name}" + ] + end end end diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb new file mode 100644 index 0000000000..c8c39db11b --- /dev/null +++ b/app/models/abuse_report.rb @@ -0,0 +1,9 @@ +class AbuseReport < ActiveRecord::Base + belongs_to :reporter, class_name: "User" + belongs_to :user + + validates :reporter, presence: true + validates :user, presence: true + validates :message, presence: true + validates :user_id, uniqueness: { scope: :reporter_id } +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 0d8365c4ff..6d1ad82a26 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -2,13 +2,11 @@ # # Table name: application_settings # -# id :integer not null, primary key +# id :integer not null, primary key # default_projects_limit :integer -# default_branch_protection :integer # signup_enabled :boolean # signin_enabled :boolean # gravatar_enabled :boolean -# twitter_sharing_enabled :boolean # sign_in_text :text # created_at :datetime # updated_at :datetime @@ -16,17 +14,34 @@ # default_branch_protection :integer default(2) # twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text -# max_attachment_size :integer default(10) +# version_check_enabled :boolean default(TRUE) +# max_attachment_size :integer default(10), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null # class ApplicationSetting < ActiveRecord::Base serialize :restricted_visibility_levels + serialize :restricted_signup_domains, Array + attr_accessor :restricted_signup_domains_raw + + validates :session_expire_delay, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :home_page_url, allow_blank: true, format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :home_page_url_column_exist + validates :after_sign_out_path, + allow_blank: true, + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -51,11 +66,32 @@ class ApplicationSetting < ActiveRecord::Base gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], - max_attachment_size: Settings.gitlab['max_attachment_size'] + max_attachment_size: Settings.gitlab['max_attachment_size'], + session_expire_delay: Settings.gitlab['session_expire_delay'], + default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + restricted_signup_domains: Settings.gitlab['restricted_signup_domains'] ) end def home_page_url_column_exist ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) end + + def restricted_signup_domains_raw + self.restricted_signup_domains.join("\n") unless self.restricted_signup_domains.nil? + end + + def restricted_signup_domains_raw=(values) + self.restricted_signup_domains = [] + self.restricted_signup_domains = values.split( + /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace + | # or + \s # any whitespace character + | # or + [\r\n] # any number of newline characters + /x) + self.restricted_signup_domains.reject! { |d| d.empty? } + end + end diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb new file mode 100644 index 0000000000..0ed0dd98a5 --- /dev/null +++ b/app/models/audit_event.rb @@ -0,0 +1,33 @@ +# == Schema Information +# +# Table name: audit_events +# +# id :integer not null, primary key +# author_id :integer not null +# type :string(255) not null +# entity_id :integer not null +# entity_type :string(255) not null +# details :text +# created_at :datetime +# updated_at :datetime +# + +class AuditEvent < ActiveRecord::Base + serialize :details, Hash + + belongs_to :user, foreign_key: :author_id + + validates :author_id, presence: true + validates :entity_id, presence: true + validates :entity_type, presence: true + + after_initialize :initialize_details + + def initialize_details + self.details = {} if details.nil? + end + + def author_name + self.user.name + end +end diff --git a/app/models/commit.rb b/app/models/commit.rb index 006fa62c8f..aff329d71f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,10 +1,16 @@ class Commit - include ActiveModel::Conversion - include StaticModel extend ActiveModel::Naming + + include ActiveModel::Conversion include Mentionable + include Participable + include Referable + include StaticModel attr_mentionable :safe_message + participant :author, :committer, :notes, :mentioned_users + + attr_accessor :project # Safe amount of changes (files and lines) in one commit to render # Used to prevent 500 error on huge commits by suppressing diff @@ -18,12 +24,12 @@ class Commit DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES) class << self - def decorate(commits) + def decorate(commits, project) commits.map do |commit| if commit.kind_of?(Commit) commit else - self.new(commit) + self.new(commit, project) end end end @@ -41,16 +47,45 @@ class Commit attr_accessor :raw - def initialize(raw_commit) + def initialize(raw_commit, project) raise "Nil as raw commit passed" unless raw_commit @raw = raw_commit + @project = project end def id @raw.id end + def ==(other) + (self.class === other) && (raw == other.raw) + end + + def self.reference_prefix + '@' + end + + # Pattern used to extract commit references from text + # + # The SHA can be between 6 and 40 hex characters. + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (?:#{Project.reference_pattern}#{reference_prefix})? + (?\h{6,40}) + }x + end + + def to_reference(from_project = nil) + if cross_project_reference?(from_project) + "#{project.to_reference}@#{id}" + else + id + end + end + def diff_line_count @diff_line_count ||= Commit::diff_line_count(self.diffs) @diff_line_count @@ -100,7 +135,7 @@ class Commit description.present? end - def hook_attrs(project) + def hook_attrs path_with_namespace = project.path_with_namespace { @@ -117,31 +152,28 @@ class Commit # Discover issues should be closed when this commit is pushed to a project's # default branch. - def closes_issues(project, current_user = self.committer) + def closes_issues(current_user = self.committer) Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) end - # Mentionable override. - def gfm_reference - "commit #{id}" - end - def author - User.find_for_commit(author_email, author_name) + @author ||= User.find_by_any_email(author_email) end def committer - User.find_for_commit(committer_email, committer_name) + @committer ||= User.find_by_any_email(committer_email) + end + + def notes + project.notes.for_commit_id(self.id) end def method_missing(m, *args, &block) @raw.send(m, *args, &block) end - def respond_to?(method) - return true if @raw.respond_to?(method) - - super + def respond_to_missing?(method, include_private = false) + @raw.respond_to?(method, include_private) || super end # Truncate sha to 8 characters @@ -150,6 +182,6 @@ class Commit end def parents - @parents ||= Commit.decorate(super) + @parents ||= Commit.decorate(super, project) end end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb new file mode 100644 index 0000000000..86fc9eb01a --- /dev/null +++ b/app/models/commit_range.rb @@ -0,0 +1,132 @@ +# CommitRange makes it easier to work with commit ranges +# +# Examples: +# +# range = CommitRange.new('f3f85602...e86e1013') +# range.exclude_start? # => false +# range.reference_title # => "Commits f3f85602 through e86e1013" +# range.to_s # => "f3f85602...e86e1013" +# +# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae') +# range.exclude_start? # => true +# range.reference_title # => "Commits f3f85602^ through e86e1013" +# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} +# range.to_s # => "f3f85602..e86e1013" +# +# # Assuming `project` is a Project with a repository containing both commits: +# range.project = project +# range.valid_commits? # => true +# +class CommitRange + include ActiveModel::Conversion + include Referable + + attr_reader :sha_from, :notation, :sha_to + + # Optional Project model + attr_accessor :project + + # See `exclude_start?` + attr_reader :exclude_start + + # The beginning and ending SHAs can be between 6 and 40 hex characters, and + # the range notation can be double- or triple-dot. + PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + + def self.reference_prefix + '@' + end + + # Pattern used to extract commit range references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (?:#{Project.reference_pattern}#{reference_prefix})? + (?#{PATTERN}) + }x + end + + # Initialize a CommitRange + # + # range_string - The String commit range. + # project - An optional Project model. + # + # Raises ArgumentError if `range_string` does not match `PATTERN`. + def initialize(range_string, project = nil) + range_string.strip! + + unless range_string.match(/\A#{PATTERN}\z/) + raise ArgumentError, "invalid CommitRange string format: #{range_string}" + end + + @exclude_start = !range_string.include?('...') + @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2) + + @project = project + end + + def inspect + %(#<#{self.class}:#{object_id} #{to_s}>) + end + + def to_s + "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" + end + + def to_reference(from_project = nil) + # Not using to_s because we want the full SHAs + reference = sha_from + notation + sha_to + + if cross_project_reference?(from_project) + reference = project.to_reference + '@' + reference + end + + reference + end + + # Returns a String for use in a link's title attribute + def reference_title + "Commits #{suffixed_sha_from} through #{sha_to}" + end + + # Return a Hash of parameters for passing to a URL helper + # + # See `namespace_project_compare_url` + def to_param + { from: suffixed_sha_from, to: sha_to } + end + + def exclude_start? + exclude_start + end + + # Check if both the starting and ending commit IDs exist in a project's + # repository + # + # project - An optional Project to check (default: `project`) + def valid_commits?(project = project) + return nil unless project.present? + return false unless project.valid_repo? + + commit_from.present? && commit_to.present? + end + + def persisted? + true + end + + def commit_from + @commit_from ||= project.repository.commit(suffixed_sha_from) + end + + def commit_to + @commit_to ||= project.repository.commit(sha_to) + end + + private + + def suffixed_sha_from + sha_from + (exclude_start? ? '^' : '') + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 478134dff6..40642dc63b 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -7,10 +7,12 @@ module Issuable extend ActiveSupport::Concern include Mentionable + include Participable included do belongs_to :author, class_name: "User" belongs_to :assignee, class_name: "User" + belongs_to :updated_by, class_name: "User" belongs_to :milestone has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy @@ -45,6 +47,7 @@ module Issuable prefix: true attr_mentionable :title, :description + participant :author, :assignee, :notes, :mentioned_users end module ClassMethods @@ -117,22 +120,6 @@ module Issuable upvotes + downvotes end - # Return all users participating on the discussion - def participants(current_user = self.author) - users = [] - users << author - users << assignee if is_assigned? - mentions = [] - mentions << self.mentioned_users(current_user) - - notes.each do |note| - users << note.author - mentions << note.mentioned_users(current_user) - end - - users.concat(mentions.reduce([], :|)).uniq - end - def subscribed?(user) subscription = subscriptions.find_by_user_id(user.id) @@ -173,6 +160,16 @@ module Issuable end end + # Convert this Issuable class name to a format usable by Ability definitions + # + # Examples: + # + # issuable.class # => MergeRequest + # issuable.to_ability_name # => "merge_request" + def to_ability_name + self.class.to_s.underscore + end + private def filter_superceded_votes(votes, notes) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index b7882a2bb1..5b0ae41164 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -20,15 +20,20 @@ module Mentionable end end - # Generate a GFM back-reference that will construct a link back to this Mentionable when rendered. Must - # be overridden if this model object can be referenced directly by GFM notation. - def gfm_reference - raise NotImplementedError.new("#{self.class} does not implement #gfm_reference") + # Returns the text used as the body of a Note when this object is referenced + # + # By default this will be the class name and the result of calling + # `to_reference` on the object. + def gfm_reference(from_project = nil) + # "MergeRequest" > "merge_request" > "Merge request" > "merge request" + friendly_name = self.class.to_s.underscore.humanize.downcase + + "#{friendly_name} #{to_reference(from_project)}" end # Construct a String that contains possible GFM references. def mentionable_text - self.class.mentionable_attrs.map { |attr| send(attr) || '' }.join + self.class.mentionable_attrs.map { |attr| send(attr) }.compact.join("\n\n") end # The GFM reference to this Mentionable, which shouldn't be included in its #references. @@ -39,7 +44,7 @@ module Mentionable # Determine whether or not a cross-reference Note has already been created between this Mentionable and # the specified target. def has_mentioned?(target) - Note.cross_reference_exists?(target, local_reference) + SystemNoteService.cross_reference_exists?(target, local_reference) end def mentioned_users(current_user = nil) @@ -62,28 +67,48 @@ module Mentionable # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. def create_cross_references!(p = project, a = author, without = []) - refs = references(p) - without + refs = references(p) + + # We're using this method instead of Array diffing because that requires + # both of the object's `hash` values to be the same, which may not be the + # case for otherwise identical Commit objects. + refs.reject! { |ref| without.include?(ref) } + refs.each do |ref| - Note.create_cross_reference_note(ref, local_reference, a, p) + SystemNoteService.cross_reference(ref, local_reference, a) end end - # If the mentionable_text field is about to change, locate any *added* references and create cross references for - # them. Invoke from an observer's #before_save implementation. - def notice_added_references(p = project, a = author) - ch = changed_attributes - original, mentionable_changed = "", false - self.class.mentionable_attrs.each do |attr| - if ch[attr] - original << ch[attr] - mentionable_changed = true - end - end + # When a mentionable field is changed, creates cross-reference notes that + # don't already exist + def create_new_cross_references!(p = project, a = author) + changes = detect_mentionable_changes - # Only proceed if the saved changes actually include a chance to an attr_mentionable field. - return unless mentionable_changed + return if changes.empty? - preexisting = references(p, self.author, original) + original_text = changes.collect { |_, vals| vals.first }.join(' ') + + preexisting = references(p, self.author, original_text) create_cross_references!(p, a, preexisting) end + + private + + # Returns a Hash of changed mentionable fields + # + # Preference is given to the `changes` Hash, but falls back to + # `previous_changes` if it's empty (i.e., the changes have already been + # persisted). + # + # See ActiveModel::Dirty. + # + # Returns a Hash. + def detect_mentionable_changes + source = (changes.present? ? changes : previous_changes).dup + + mentionable = self.class.mentionable_attrs + + # Only include changed fields that are mentionable + source.select { |key, val| mentionable.include?(key) } + end end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb new file mode 100644 index 0000000000..7c9597333d --- /dev/null +++ b/app/models/concerns/participable.rb @@ -0,0 +1,75 @@ +# == Participable concern +# +# Contains functionality related to objects that can have participants, such as +# an author, an assignee and people mentioned in its description or comments. +# +# Used by Issue, Note, MergeRequest, Snippet and Commit. +# +# Usage: +# +# class Issue < ActiveRecord::Base +# include Participable +# +# # ... +# +# participant :author, :assignee, :mentioned_users, :notes +# end +# +# issue = Issue.last +# users = issue.participants +# # `users` will contain the issue's author, its assignee, +# # all users returned by its #mentioned_users method, +# # as well as all participants to all of the issue's notes, +# # since Note implements Participable as well. +# +module Participable + extend ActiveSupport::Concern + + module ClassMethods + def participant(*attrs) + participant_attrs.concat(attrs.map(&:to_s)) + end + + def participant_attrs + @participant_attrs ||= [] + end + end + + # Be aware that this method makes a lot of sql queries. + # Save result into variable if you are going to reuse it inside same request + def participants(current_user = self.author, project = self.project) + participants = self.class.participant_attrs.flat_map do |attr| + meth = method(attr) + + value = + if meth.arity == 1 || meth.arity == -1 + meth.call(current_user) + else + meth.call + end + + participants_for(value, current_user, project) + end.compact.uniq + + if project + participants.select! do |user| + user.can?(:read_project, project) + end + end + + participants + end + + private + + def participants_for(value, current_user = nil, project = nil) + case value + when User + [value] + when Enumerable, ActiveRecord::Relation + value.flat_map { |v| participants_for(v, current_user, project) } + when Participable + value.participants(current_user, project) + end + end +end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb new file mode 100644 index 0000000000..cced66cc1e --- /dev/null +++ b/app/models/concerns/referable.rb @@ -0,0 +1,61 @@ +# == Referable concern +# +# Contains functionality related to making a model referable in Markdown, such +# as "#1", "!2", "~3", etc. +module Referable + extend ActiveSupport::Concern + + # Returns the String necessary to reference this object in Markdown + # + # from_project - Refering Project object + # + # This should be overridden by the including class. + # + # Examples: + # + # Issue.first.to_reference # => "#1" + # Issue.last.to_reference(other_project) # => "cross-project#1" + # + # Returns a String + def to_reference(_from_project = nil) + '' + end + + module ClassMethods + # The character that prefixes the actual reference identifier + # + # This should be overridden by the including class. + # + # Examples: + # + # Issue.reference_prefix # => '#' + # MergeRequest.reference_prefix # => '!' + # + # Returns a String + def reference_prefix + '' + end + + # Regexp pattern used to match references to this object + # + # This must be overridden by the including class. + # + # Returns a Regexp + def reference_pattern + raise NotImplementedError, "#{self} does not implement #{__method__}" + end + end + + private + + # Check if a reference is being done cross-project + # + # from_project - Refering Project object + def cross_project_reference?(from_project) + if self.is_a?(Project) + self != from_project + else + from_project && self.project && self.project != from_project + end + end +end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index bbb3b301a9..660e58b876 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -1,51 +1,37 @@ +require 'task_list' +require 'task_list/filter' + # Contains functionality for objects that can have task lists in their # descriptions. Task list items can be added with Markdown like "* [x] Fix # bugs". # # Used by MergeRequest and Issue module Taskable - TASK_PATTERN_MD = /^(? *[*-] *)\[(?[ xX])\]/.freeze - TASK_PATTERN_HTML = /^
  • (?\s*

    )?\[(?[ xX])\]/.freeze + # Called by `TaskList::Summary` + def task_list_items + return [] if description.blank? - # Change the state of a task list item for this Taskable. Edit the object's - # description by finding the nth task item and changing its checkbox - # placeholder to "[x]" if +checked+ is true, or "[ ]" if it's false. - # Note: task numbering starts with 1 - def update_nth_task(n, checked) - index = 0 - check_char = checked ? 'x' : ' ' - - # Do this instead of using #gsub! so that ActiveRecord detects that a field - # has changed. - self.description = self.description.gsub(TASK_PATTERN_MD) do |match| - index += 1 - case index - when n then "#{$LAST_MATCH_INFO[:bullet]}[#{check_char}]" - else match - end + @task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item| + # ItemPattern strips out the hyphen, but Item requires it. Rabble rabble. + TaskList::Item.new("- #{item}") end + end - save + def tasks + @tasks ||= TaskList.new(self) end # Return true if this object's description has any task list items. def tasks? - description && description.match(TASK_PATTERN_MD) + tasks.summary.items? end # Return a string that describes the current state of this Taskable's task - # list items, e.g. "20 tasks (12 done, 8 unfinished)" + # list items, e.g. "20 tasks (12 completed, 8 remaining)" def task_status - return nil unless description + return '' if description.blank? - num_tasks = 0 - num_done = 0 - - description.scan(TASK_PATTERN_MD) do - num_tasks += 1 - num_done += 1 unless $LAST_MATCH_INFO[:checked] == ' ' - end - - "#{num_tasks} tasks (#{num_done} done, #{num_tasks - num_done} unfinished)" + sum = tasks.summary + "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)" end end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 85d52d558c..9ab663c04a 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -7,10 +7,10 @@ # created_at :datetime # updated_at :datetime # key :text -# public :boolean default(FALSE) # title :string(255) # type :string(255) # fingerprint :string(255) +# public :boolean default(FALSE), not null # class DeployKey < Key diff --git a/app/models/email.rb b/app/models/email.rb index 556b0e9586..935705e2ed 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -18,7 +18,6 @@ class Email < ActiveRecord::Base validates :email, presence: true, email: { strict_mode: true }, uniqueness: true validate :unique_email, if: ->(email) { email.email_changed? } - after_create :notify before_validation :cleanup_email def cleanup_email @@ -28,8 +27,4 @@ class Email < ActiveRecord::Base def unique_email self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) end - - def notify - NotificationService.new.new_email(self) - end end diff --git a/app/models/event.rb b/app/models/event.rb index c9a88ffa8e..78f16c6304 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -44,7 +44,7 @@ class Event < ActiveRecord::Base after_create :reset_project_activity # Scopes - scope :recent, -> { order("created_at DESC") } + scope :recent, -> { order(created_at: :desc) } scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } scope :with_associations, -> { includes(project: :namespace) } diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 50efcb32f1..49f6c95e04 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -1,4 +1,6 @@ class ExternalIssue + include Referable + def initialize(issue_identifier, project) @issue_identifier, @project = issue_identifier, project end @@ -15,6 +17,10 @@ class ExternalIssue @issue_identifier.to_s end + def title + "External Issue #{self}" + end + def ==(other) other.is_a?(self.class) && (to_s == other.to_s) end @@ -22,4 +28,13 @@ class ExternalIssue def project @project end + + # Pattern used to extract `JIRA-123` issue references from text + def self.reference_pattern + %r{(?([A-Z\-]+-)\d+)} + end + + def to_reference(_from_project = nil) + id + end end diff --git a/app/models/group.rb b/app/models/group.rb index 1386a9eccc..9cd146bb73 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -17,10 +17,13 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Group < Namespace + include Gitlab::ConfigHelper + include Referable + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :users, through: :group_members - validate :avatar_type, if: ->(user) { user.avatar_changed? } + validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AvatarUploader @@ -36,12 +39,30 @@ class Group < Namespace def sort(method) order_by(method) end + + def reference_prefix + User.reference_prefix + end + + def reference_pattern + User.reference_pattern + end + end + + def to_reference(_from_project = nil) + "#{self.class.reference_prefix}#{name}" end def human_name name end + def avatar_url(size = nil) + if avatar.present? + [gitlab_config.url, avatar.url].join + end + end + def owners @owners ||= group_members.owners.map(&:user) end @@ -56,8 +77,24 @@ class Group < Namespace add_users([user], access_level, current_user) end + def add_guest(user, current_user = nil) + add_user(user, Gitlab::Access::GUEST, current_user) + end + + def add_reporter(user, current_user = nil) + add_user(user, Gitlab::Access::REPORTER, current_user) + end + + def add_developer(user, current_user = nil) + add_user(user, Gitlab::Access::DEVELOPER, current_user) + end + + def add_master(user, current_user = nil) + add_user(user, Gitlab::Access::MASTER, current_user) + end + def add_owner(user, current_user = nil) - self.add_user(user, Gitlab::Access::OWNER, current_user) + add_user(user, Gitlab::Access::OWNER, current_user) end def has_owner?(user) @@ -87,10 +124,14 @@ class Group < Namespace end def post_create_hook + Gitlab::AppLogger.info("Group \"#{name}\" was created") + system_hook_service.execute_hooks_for(self, :create) end def post_destroy_hook + Gitlab::AppLogger.info("Group \"#{name}\" was removed") + system_hook_service.execute_hooks_for(self, :destroy) end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index 7e4f16ebf1..ab055f6b80 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -44,7 +44,7 @@ class GroupMilestone def percent_complete ((closed_items_count * 100) / total_items_count).abs rescue ZeroDivisionError - 100 + 0 end def state diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 21867a9316..ca7066b959 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # class ProjectHook < WebHook @@ -21,5 +22,6 @@ class ProjectHook < WebHook scope :push_hooks, -> { where(push_events: true) } scope :tag_push_hooks, -> { where(tag_push_events: true) } scope :issue_hooks, -> { where(issues_events: true) } + scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } end diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 2e11239c40..b55e217975 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -13,8 +13,13 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # class ServiceHook < WebHook belongs_to :service + + def execute(data) + super(data, 'service_hook') + end end diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index ee32b49bc6..6fb2d42102 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # class SystemHook < WebHook diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 315d96af1b..46fb85336e 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # class WebHook < ActiveRecord::Base @@ -21,6 +22,7 @@ class WebHook < ActiveRecord::Base default_value_for :push_events, true default_value_for :issues_events, false + default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false @@ -30,12 +32,15 @@ class WebHook < ActiveRecord::Base validates :url, presence: true, format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } - def execute(data) + def execute(data, hook_name) parsed_url = URI.parse(url) if parsed_url.userinfo.blank? WebHook.post(url, body: data.to_json, - headers: { "Content-Type" => "application/json" }, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, verify: false) else post_url = url.gsub("#{parsed_url.userinfo}@", "") @@ -45,7 +50,10 @@ class WebHook < ActiveRecord::Base } WebHook.post(post_url, body: data.to_json, - headers: { "Content-Type" => "application/json" }, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, verify: false, basic_auth: auth) end @@ -54,7 +62,7 @@ class WebHook < ActiveRecord::Base false end - def async_execute(data) - Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data) + def async_execute(data, hook_name) + Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name) end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 756d19adec..ad60154be7 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -14,6 +14,7 @@ class Identity < ActiveRecord::Base include Sortable belongs_to :user + validates :provider, presence: true validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: :provider } end diff --git a/app/models/issue.rb b/app/models/issue.rb index 6e10205138..2456b7d0dc 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -21,10 +21,11 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Issue < ActiveRecord::Base - include Issuable include InternalId - include Taskable + include Issuable + include Referable include Sortable + include Taskable ActsAsTaggableOn.strict_case_match = true @@ -53,10 +54,28 @@ class Issue < ActiveRecord::Base attributes end - # Mentionable overrides. + def self.reference_prefix + '#' + end - def gfm_reference - "issue ##{iid}" + # Pattern used to extract `#123` issue references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{iid}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference end # Reset issue events cache diff --git a/app/models/key.rb b/app/models/key.rb index 016eee8699..406a1257b5 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -10,6 +10,7 @@ # title :string(255) # type :string(255) # fingerprint :string(255) +# public :boolean default(FALSE), not null # require 'digest/md5' @@ -23,6 +24,7 @@ class Key < ActiveRecord::Base validates :title, presence: true, length: { within: 0..255 } validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true + validates :key, format: { without: /\n|\r/, message: 'should be a single line' } validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } delegate :name, :email, to: :user, prefix: true @@ -37,6 +39,11 @@ class Key < ActiveRecord::Base self.key = key.strip unless key.blank? end + def publishable_key + #Removes anything beyond the keytype and key itself + self.key.split[0..1].join(' ') + end + # projects that has this key def projects user.authorized_projects diff --git a/app/models/label.rb b/app/models/label.rb index 1f22ed23d4..230631b518 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -11,6 +11,8 @@ # class Label < ActiveRecord::Base + include Referable + DEFAULT_COLOR = '#428BCA' default_value_for :color, DEFAULT_COLOR @@ -27,13 +29,52 @@ class Label < ActiveRecord::Base # Don't allow '?', '&', and ',' for label titles validates :title, presence: true, - format: { with: /\A[^&\?,&]+\z/ }, + format: { with: /\A[^&\?,]+\z/ }, uniqueness: { scope: :project_id } default_scope { order(title: :asc) } alias_attribute :name, :title + def self.reference_prefix + '~' + end + + # Pattern used to extract label references from text + def self.reference_pattern + %r{ + #{reference_prefix} + (?: + (?\d+) | # Integer-based label ID, or + (? + [A-Za-z0-9_-]+ | # String-based single-word label title, or + "[^&\?,]+" # String-based multi-word label surrounded in quotes + ) + ) + }x + end + + # Returns the String necessary to reference this Label in Markdown + # + # format - Symbol format to use (default: :id, optional: :name) + # + # Note that its argument differs from other objects implementing Referable. If + # a non-Symbol argument is given (such as a Project), it will default to :id. + # + # Examples: + # + # Label.first.to_reference # => "~1" + # Label.first.to_reference(:name) # => "~\"bug\"" + # + # Returns a String + def to_reference(format = :id) + if format == :name && !name.include?('"') + %(#{self.class.reference_prefix}"#{name}") + else + "#{self.class.reference_prefix}#{id}" + end + end + def open_issues_count issues.opened.count end diff --git a/app/models/member.rb b/app/models/member.rb index d151c7b239..cae8caa23f 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -6,14 +6,14 @@ # access_level :integer not null # source_id :integer not null # source_type :string(255) not null -# user_id :integer not null +# user_id :integer # notification_level :integer not null # type :string(255) # created_at :datetime # updated_at :datetime # created_by_id :integer -# invite_email :string -# invite_token :string +# invite_email :string(255) +# invite_token :string(255) # invite_accepted_at :datetime # diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 84c91372b3..65d2ea0057 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -6,11 +6,15 @@ # access_level :integer not null # source_id :integer not null # source_type :string(255) not null -# user_id :integer not null +# user_id :integer # notification_level :integer not null # type :string(255) # created_at :datetime # updated_at :datetime +# created_by_id :integer +# invite_email :string(255) +# invite_token :string(255) +# invite_accepted_at :datetime # class GroupMember < Member diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 0a3b4d2182..1b0c76917a 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -6,11 +6,15 @@ # access_level :integer not null # source_id :integer not null # source_type :string(255) not null -# user_id :integer not null +# user_id :integer # notification_level :integer not null # type :string(255) # created_at :datetime # updated_at :datetime +# created_by_id :integer +# invite_email :string(255) +# invite_token :string(255) +# invite_accepted_at :datetime # class ProjectMember < Member diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 9c9e276250..324d1795ab 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -25,10 +25,11 @@ require Rails.root.join("app/models/commit") require Rails.root.join("lib/static_model") class MergeRequest < ActiveRecord::Base - include Issuable - include Taskable include InternalId + include Issuable + include Referable include Sortable + include Taskable belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" @@ -124,16 +125,38 @@ class MergeRequest < ActiveRecord::Base validate :validate_fork scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) } - scope :merged, -> { with_state(:merged) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) } - # Closed scope for merge request should return - # both merged and closed mr's - scope :closed, -> { with_states(:closed, :merged) } - scope :declined, -> { with_states(:closed) } + scope :merged, -> { with_state(:merged) } + scope :closed, -> { with_state(:closed) } + scope :closed_and_merged, -> { with_states(:closed, :merged) } + + def self.reference_prefix + '!' + end + + # Pattern used to extract `!123` merge request references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{iid}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference + end def validate_branches if target_project == source_project && target_branch == source_branch @@ -172,7 +195,6 @@ class MergeRequest < ActiveRecord::Base def update_merge_request_diff if source_branch_changed? || target_branch_changed? reload_code - mark_as_unchecked end end @@ -199,24 +221,49 @@ class MergeRequest < ActiveRecord::Base end def automerge!(current_user, commit_message = nil) + return unless automergeable? + MergeRequests::AutoMergeService. new(target_project, current_user). execute(self, commit_message) end + def remove_source_branch? + self.should_remove_source_branch && !self.source_project.root_ref?(self.source_branch) && !self.for_fork? + end + def open? opened? || reopened? end + def work_in_progress? + title =~ /\A\[?WIP\]?:? /i + end + + def automergeable? + open? && !work_in_progress? && can_be_merged? + end + + def automerge_status + if work_in_progress? + "work_in_progress" + else + merge_status_name + end + end + def mr_and_commit_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 commit_ids = commits.last(commits_for_notes_limit).map(&:id) - project.notes.where( - "(noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR (noteable_type = 'Commit' AND commit_id IN (:commit_ids))", + Note.where( + "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" + + "(project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", mr_id: id, - commit_ids: commit_ids + commit_ids: commit_ids, + target_project_id: target_project_id, + source_project_id: source_project_id ) end @@ -242,7 +289,7 @@ class MergeRequest < ActiveRecord::Base } unless last_commit.nil? - attrs.merge!(last_commit: last_commit.hook_attrs(source_project)) + attrs.merge!(last_commit: last_commit.hook_attrs) end attributes.merge!(attrs) @@ -259,7 +306,7 @@ class MergeRequest < ActiveRecord::Base # Return the set of issues that will be closed if this merge request is accepted. def closes_issues(current_user = self.author) if target_branch == project.default_branch - issues = commits.flat_map { |c| c.closes_issues(project, current_user) } + issues = commits.flat_map { |c| c.closes_issues(current_user) } issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). closed_by_message(description)) issues.uniq.sort_by(&:id) @@ -268,11 +315,6 @@ class MergeRequest < ActiveRecord::Base end end - # Mentionable override. - def gfm_reference - "merge request !#{iid}" - end - def target_project_path if target_project target_project.path_with_namespace @@ -365,4 +407,26 @@ class MergeRequest < ActiveRecord::Base locked_at.nil? || locked_at < (Time.now - 1.day) end + + def has_ci? + source_project.ci_service && commits.any? + end + + def branch_missing? + !source_branch_exists? || !target_branch_exists? + end + + def can_be_merged_by?(user) + ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) + end + + def state_human_name + if merged? + "Merged" + elsif closed? + "Closed" + else + "Open" + end + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index acac1ca4cf..df1c2b7875 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -67,7 +67,7 @@ class MergeRequestDiff < ActiveRecord::Base end def load_commits(array) - array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash)) } + array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } end def dump_diffs(diffs) @@ -88,7 +88,7 @@ class MergeRequestDiff < ActiveRecord::Base commits = compare_result.commits if commits.present? - commits = Commit.decorate(commits). + commits = Commit.decorate(commits, merge_request.source_project). sort_by(&:created_at). reverse end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 9bbb2bafb9..d28f3c8d3f 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -14,6 +14,10 @@ # class Milestone < ActiveRecord::Base + # Represents a "No Milestone" state used for filtering Issues and Merge + # Requests that have no milestone assigned. + None = Struct.new(:title).new('No Milestone') + include InternalId include Sortable @@ -56,7 +60,7 @@ class Milestone < ActiveRecord::Base end def closed_items_count - self.issues.closed.count + self.merge_requests.closed.count + self.issues.closed.count + self.merge_requests.closed_and_merged.count end def total_items_count @@ -66,7 +70,7 @@ class Milestone < ActiveRecord::Base def percent_complete ((closed_items_count * 100) / total_items_count).abs rescue ZeroDivisionError - 100 + 0 end def expires_at diff --git a/app/models/namespace.rb b/app/models/namespace.rb index e1de114375..30ffacadde 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -60,15 +60,24 @@ class Namespace < ActiveRecord::Base def clean_path(path) path = path.dup + # Get the email username by removing everything after an `@` sign. path.gsub!(/@.*\z/, "") + # Usernames can't end in .git, so remove it. path.gsub!(/\.git\z/, "") + # Remove dashes at the start of the username. path.gsub!(/\A-+/, "") + # Remove periods at the end of the username. path.gsub!(/\.+\z/, "") + # Remove everything that's not in the list of allowed characters. path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + # Users with the great usernames of "." or ".." would end up with a blank username. + # Work around that by setting their username to "blank", followed by a counter. + path = "blank" if path.blank? + counter = 0 base = path - while Namespace.by_path(path).present? + while Namespace.find_by_path_or_name(path) counter += 1 path = "#{base}#{counter}" end @@ -90,10 +99,24 @@ class Namespace < ActiveRecord::Base end def rm_dir - gitlab_shell.rm_namespace(path) + # Move namespace directory into trash. + # We will remove it later async + new_path = "#{path}+#{id}+deleted" + + if gitlab_shell.mv_namespace(path, new_path) + message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" + Gitlab::AppLogger.info message + + # Remove namespace directroy async with delay so + # GitLab has time to remove all projects first + GitlabShellWorker.perform_in(5.minutes, :rm_namespace, new_path) + end end def move_dir + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(path_was) + if gitlab_shell.mv_namespace(path_was, path) # If repositories moved successfully we need to remove old satellites # and send update instructions to users. diff --git a/app/models/note.rb b/app/models/note.rb index 2cf3fac2de..913a8c0033 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -23,14 +23,17 @@ require 'file_size_validator' class Note < ActiveRecord::Base include Mentionable include Gitlab::CurrentSettings + include Participable default_value_for :system, false attr_mentionable :note + participant :author, :mentioned_users belongs_to :project belongs_to :noteable, polymorphic: true belongs_to :author, class_name: "User" + belongs_to :updated_by, class_name: "User" delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true @@ -61,145 +64,6 @@ class Note < ActiveRecord::Base after_update :set_references class << self - def create_status_change_note(noteable, project, author, status, source) - body = "Status changed to #{status}#{' by ' + source.gfm_reference if source}" - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - - # +noteable+ was referenced from +mentioner+, by including GFM in either - # +mentioner+'s description or an associated Note. - # Create a system Note associated with +noteable+ with a GFM back-reference - # to +mentioner+. - def create_cross_reference_note(noteable, mentioner, author, project) - gfm_reference = mentioner_gfm_ref(noteable, mentioner, project) - - note_options = { - project: project, - author: author, - note: cross_reference_note_content(gfm_reference), - system: true - } - - if noteable.kind_of?(Commit) - note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) - else - note_options.merge!(noteable: noteable) - end - - create(note_options) unless cross_reference_disallowed?(noteable, mentioner) - end - - def create_milestone_change_note(noteable, project, author, milestone) - body = if milestone.nil? - 'Milestone removed' - else - "Milestone changed to #{milestone.title}" - end - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - - def create_assignee_change_note(noteable, project, author, assignee) - body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}" - - create({ - noteable: noteable, - project: project, - author: author, - note: body, - system: true - }) - end - - def create_labels_change_note(noteable, project, author, added_labels, removed_labels) - labels_count = added_labels.count + removed_labels.count - added_labels = added_labels.map{ |label| "~#{label.id}" }.join(' ') - removed_labels = removed_labels.map{ |label| "~#{label.id}" }.join(' ') - message = '' - - if added_labels.present? - message << "added #{added_labels}" - end - - if added_labels.present? && removed_labels.present? - message << ' and ' - end - - if removed_labels.present? - message << "removed #{removed_labels}" - end - - message << ' ' << 'label'.pluralize(labels_count) - body = "#{message.capitalize}" - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - - def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = [], oldrev = nil) - total_count = new_commits.length + existing_commits.length - commits_text = ActionController::Base.helpers.pluralize(total_count, 'commit') - body = "Added #{commits_text}:\n\n" - - if existing_commits.length > 0 - commit_ids = - if existing_commits.length == 1 - existing_commits.first.short_id - else - if oldrev - "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}" - else - "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" - end - end - - commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit') - - branch = - if merge_request.for_fork? - "#{merge_request.target_project_namespace}:#{merge_request.target_branch}" - else - merge_request.target_branch - end - - message = "* #{commit_ids} - #{commits_text} from branch `#{branch}`" - body << message - body << "\n" - end - - new_commits.each do |commit| - message = "* #{commit.short_id} - #{commit.title}" - body << message - body << "\n" - end - - create( - noteable: merge_request, - project: project, - author: author, - note: body, - system: true - ) - end - def discussions_from_notes(notes) discussion_ids = [] discussions = [] @@ -225,120 +89,19 @@ class Note < ActiveRecord::Base [:discussion, type.try(:underscore), id, line_code].join("-").to_sym end - # Determine if cross reference note should be created. - # eg. mentioning a commit in MR comments which exists inside a MR - # should not create "mentioned in" note. - def cross_reference_disallowed?(noteable, mentioner) - if mentioner.kind_of?(MergeRequest) - mentioner.commits.map(&:id).include? noteable.id - end - end - - # Determine whether or not a cross-reference note already exists. - def cross_reference_exists?(noteable, mentioner) - gfm_reference = mentioner_gfm_ref(noteable, mentioner) - notes = if noteable.is_a?(Commit) - where(commit_id: noteable.id) - else - where(noteable_id: noteable.id) - end - - notes.where('note like ?', cross_reference_note_pattern(gfm_reference)). - system.any? - end - def search(query) where("note like :query", query: "%#{query}%") end + end - def cross_reference_note_prefix - 'mentioned in ' - end - - private - - def cross_reference_note_content(gfm_reference) - cross_reference_note_prefix + "#{gfm_reference}" - end - - def cross_reference_note_pattern(gfm_reference) - # Older cross reference notes contained underscores for emphasis - "%" + cross_reference_note_content(gfm_reference) + "%" - end - - # Prepend the mentioner's namespaced project path to the GFM reference for - # cross-project references. For same-project references, return the - # unmodified GFM reference. - def mentioner_gfm_ref(noteable, mentioner, project = nil) - if mentioner.is_a?(Commit) - if project.nil? - return mentioner.gfm_reference.sub('commit ', 'commit %') - else - mentioning_project = project - end - else - mentioning_project = mentioner.project - end - - noteable_project_id = noteable_project_id(noteable, mentioning_project) - - full_gfm_reference(mentioning_project, noteable_project_id, mentioner) - end - - # Return the ID of the project that +noteable+ belongs to, or nil if - # +noteable+ is a commit and is not part of the project that owns - # +mentioner+. - def noteable_project_id(noteable, mentioning_project) - if noteable.is_a?(Commit) - if mentioning_project.repository.commit(noteable.id) - # The noteable commit belongs to the mentioner's project - mentioning_project.id - else - nil - end - else - noteable.project.id - end - end - - # Return the +mentioner+ GFM reference. If the mentioner and noteable - # projects are not the same, add the mentioning project's path to the - # returned value. - def full_gfm_reference(mentioning_project, noteable_project_id, mentioner) - if mentioning_project.id == noteable_project_id - mentioner.gfm_reference - else - if mentioner.is_a?(Commit) - mentioner.gfm_reference.sub( - /(commit )/, - "\\1#{mentioning_project.path_with_namespace}@" - ) - else - mentioner.gfm_reference.sub( - /(issue |merge request )/, - "\\1#{mentioning_project.path_with_namespace}" - ) - end - end - end + def cross_reference? + system && SystemNoteService.cross_reference?(note) end def max_attachment_size current_application_settings.max_attachment_size.megabytes.to_i end - def commit_author - @commit_author ||= - project.team.users.find_by(email: noteable.author_email) || - project.team.users.find_by(name: noteable.author_name) - rescue - nil - end - - def cross_reference? - note.start_with?(self.class.cross_reference_note_prefix) - end - def find_diff return nil unless noteable && noteable.diffs.present? @@ -479,16 +242,6 @@ class Note < ActiveRecord::Base @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) end - # Returns true if this is a downvote note, - # otherwise false is returned - def downvote? - votable? && (note.start_with?('-1') || - note.start_with?(':-1:') || - note.start_with?(':thumbsdown:') || - note.start_with?(':thumbs_down_sign:') - ) - end - def for_commit? noteable_type == "Commit" end @@ -520,7 +273,7 @@ class Note < ActiveRecord::Base # override to return commits, which are not active record def noteable if for_commit? - project.repository.commit(commit_id) + project.commit(commit_id) else super end @@ -530,14 +283,18 @@ class Note < ActiveRecord::Base nil end - # Returns true if this is an upvote note, - # otherwise false is returned + DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:) + + # Check if the note is a downvote + def downvote? + votable? && note.start_with?(*DOWNVOTES) + end + + UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:) + + # Check if the note is an upvote def upvote? - votable? && (note.start_with?('+1') || - note.start_with?(':+1:') || - note.start_with?(':thumbsup:') || - note.start_with?(':thumbs_up_sign:') - ) + votable? && note.start_with?(*UPVOTES) end def superceded?(notes) @@ -565,8 +322,8 @@ class Note < ActiveRecord::Base end # Mentionable override. - def gfm_reference - noteable.gfm_reference + def gfm_reference(from_project = nil) + noteable.gfm_reference(from_project) end # Mentionable override. @@ -600,7 +357,11 @@ class Note < ActiveRecord::Base end def set_references - notice_added_references(project, author) + create_new_cross_references!(project, author) + end + + def system? + read_attribute(:system) end def editable? diff --git a/app/models/project.rb b/app/models/project.rb index 64ee2c2212..3dc1729e81 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -21,23 +21,24 @@ # import_url :string(255) # visibility_level :integer default(0), not null # archived :boolean default(FALSE), not null +# avatar :string(255) # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) -# avatar :string(255) +# commit_count :integer default(0) # require 'carrierwave/orm/activerecord' require 'file_size_validator' class Project < ActiveRecord::Base - include Sortable + include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel - include Gitlab::ConfigHelper - include Rails.application.routes.url_helpers + include Referable + include Sortable extend Gitlab::ConfigHelper extend Enumerize @@ -144,7 +145,7 @@ class Project < ActiveRecord::Base validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :avatar_type, - if: ->(project) { project.avatar && project.avatar_changed? } + if: ->(project) { project.avatar.present? && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AvatarUploader @@ -157,7 +158,7 @@ class Project < ActiveRecord::Base scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) } scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped } scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) } - scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } + scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } scope :in_group_namespace, -> { joins(:group) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } @@ -247,6 +248,11 @@ class Project < ActiveRecord::Base order_by(method) end end + + def reference_pattern + name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR + %r{(?#{name_pattern}/#{name_pattern})} + end end def team @@ -254,7 +260,11 @@ class Project < ActiveRecord::Base end def repository - @repository ||= Repository.new(path_with_namespace) + @repository ||= Repository.new(path_with_namespace, nil, self) + end + + def commit(id = 'HEAD') + repository.commit(id) end def saved? @@ -301,8 +311,12 @@ class Project < ActiveRecord::Base path end + def to_reference(_from_project = nil) + path_with_namespace + end + def web_url - [gitlab_config.url, path_with_namespace].join('/') + Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -325,14 +339,18 @@ class Project < ActiveRecord::Base self.id end - def issue_exists?(issue_id) + def get_issue(issue_id) if default_issues_tracker? - self.issues.where(iid: issue_id).first.present? + issues.find_by(iid: issue_id) else - true + ExternalIssue.new(issue_id, self) end end + def issue_exists?(issue_id) + get_issue(issue_id) + end + def default_issue_tracker gitlab_issue_tracker_service || create_gitlab_issue_tracker_service end @@ -346,11 +364,7 @@ class Project < ActiveRecord::Base end def default_issues_tracker? - if external_issue_tracker - false - else - true - end + !external_issue_tracker end def external_issues_trackers @@ -419,7 +433,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - [gitlab_config.url, namespace_project_avatar_path(namespace, self)].join + Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self) end end @@ -479,7 +493,7 @@ class Project < ActiveRecord::Base def execute_hooks(data, hooks_scope = :push_hooks) hooks.send(hooks_scope).each do |hook| - hook.async_execute(data) + hook.async_execute(data, hooks_scope.to_s) end end @@ -557,7 +571,7 @@ class Project < ActiveRecord::Base end def http_url_to_repo - [gitlab_config.url, '/', path_with_namespace, '.git'].join('') + "#{web_url}.git" end # Check if current branch name is marked as protected in the system @@ -669,6 +683,10 @@ class Project < ActiveRecord::Base update_attribute(:repository_size, repository.size) end + def update_commit_count + update_attribute(:commit_count, repository.commit_count) + end + def forks_count ForkedProjectLink.where(forked_from_project_id: self.id).count end @@ -682,11 +700,21 @@ class Project < ActiveRecord::Base end def create_repository - if gitlab_shell.add_repository(path_with_namespace) - true + if forked? + if gitlab_shell.fork_repository(forked_from_project.path_with_namespace, self.namespace.path) + ensure_satellite_exists + true + else + errors.add(:base, 'Failed to fork repository via gitlab-shell') + false + end else - errors.add(:base, 'Failed to create repository') - false + if gitlab_shell.add_repository(path_with_namespace) + true + else + errors.add(:base, 'Failed to create repository via gitlab-shell') + false + end end end diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 6a8a8a56eb..cd3319f077 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -1,10 +1,10 @@ # == Schema Information # -# Table name: project_import_datas +# Table name: project_import_data # -# id :integer not null, primary key -# project_id :integer -# data :text +# id :integer not null, primary key +# project_id :integer +# data :text # require 'carrierwave/orm/activerecord' diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 1a36e43924..803402c83e 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # # Base class for CI services @@ -40,7 +41,7 @@ class CiService < Service # Return string with build status or :error symbol # - # Allowed states: 'success', 'failed', 'running', 'pending' + # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' # # # Ex. diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 8d25f62787..7c2027c18e 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class CustomIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 6f6e5950aa..8f5d8b086e 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class EmailsOnPushService < Service diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index a199d0e86f..9c46af7e72 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -2,14 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class ExternalWikiService < Service diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 99e361dd6e..27fc19379f 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # require "flowdock-git-hook" @@ -37,7 +38,7 @@ class FlowdockService < Service def fields [ - { type: 'text', name: 'token', placeholder: '' } + { type: 'text', name: 'token', placeholder: 'Flowdock Git source token' } ] end diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 4e75bdfc95..91ef267ad7 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # require "gemnasium/gitlab_service" diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 0f9838a575..5aaa4e85cb 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -15,14 +15,19 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class GitlabCiService < CiService API_PREFIX = "api/v1" prop_accessor :project_url, :token - validates :project_url, presence: true, if: :activated? - validates :token, presence: true, if: :activated? + validates :project_url, + presence: true, + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated? + validates :token, + presence: true, + format: { with: /\A([A-Za-z0-9]+)\z/ }, if: :activated? after_save :compose_service_hook, if: :activated? @@ -39,11 +44,21 @@ class GitlabCiService < CiService def execute(data) return unless supported_events.include?(data[:object_kind]) + sha = data[:checkout_sha] + + if sha.present? + file = ci_yaml_file(sha) + + if file && file.data + data.merge!(ci_yaml_file: file.data) + end + end + service_hook.execute(data) end def commit_status_path(sha, ref) - project_url + "/refs/#{ref}/commits/#{sha}/status.json?token=#{token}" + URI::encode(project_url + "/refs/#{ref}/commits/#{sha}/status.json?token=#{token}") end def get_ci_build(sha, ref) @@ -65,6 +80,7 @@ class GitlabCiService < CiService params = { id: new_project.id, name_with_namespace: new_project.name_with_namespace, + path_with_namespace: new_project.path_with_namespace, web_url: new_project.web_url, default_branch: new_project.default_branch, ssh_url_to_repo: new_project.ssh_url_to_repo @@ -90,7 +106,7 @@ class GitlabCiService < CiService end def build_page(sha, ref) - project_url + "/refs/#{ref}/commits/#{sha}" + URI::encode(project_url + "/refs/#{ref}/commits/#{sha}") end def builds_path @@ -122,7 +138,15 @@ class GitlabCiService < CiService private + def ci_yaml_file(sha) + repository.blob_at(sha, '.gitlab-ci.yml') + end + def fork_registration_path project_url.sub(/projects\/\d*/, "#{API_PREFIX}/forks") end + + def repository + project.repository + end end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 5f0553f3b0..0ebc0a3ba1 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -21,11 +21,6 @@ class GitlabIssueTrackerService < IssueTrackerService include Rails.application.routes.url_helpers - default_url_options[:host] = Gitlab.config.gitlab.host - default_url_options[:protocol] = Gitlab.config.gitlab.protocol - default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? - default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def default? diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index d264a56ebd..7a15a861ab 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -15,12 +15,13 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class HipchatService < Service MAX_COMMITS = 3 - prop_accessor :token, :room, :server + prop_accessor :token, :room, :server, :notify, :color, :api_version validates :token, presence: true, if: :activated? def title @@ -39,6 +40,10 @@ class HipchatService < Service [ { type: 'text', name: 'token', placeholder: 'Room token' }, { type: 'text', name: 'room', placeholder: 'Room name or ID' }, + { type: 'checkbox', name: 'notify' }, + { type: 'select', name: 'color', choices: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] }, + { type: 'text', name: 'api_version', + placeholder: 'Leave blank for default (v2)' }, { type: 'text', name: 'server', placeholder: 'Leave blank for default. https://hipchat.example.com' } ] @@ -50,18 +55,33 @@ class HipchatService < Service def execute(data) return unless supported_events.include?(data[:object_kind]) + message = create_message(data) + return unless message.present? + gate[room].send('GitLab', message, message_options) + end - gate[room].send('GitLab', create_message(data)) + def test(data) + begin + result = execute(data) + rescue StandardError => error + return { success: false, result: error } + end + + { success: true, result: result } end private def gate - options = { api_version: 'v2' } + options = { api_version: api_version.present? ? api_version : 'v2' } options[:server_url] = server unless server.blank? @gate ||= HipChat::Client.new(token, options) end + def message_options + { notify: notify.present? && notify == '1', color: color || 'yellow' } + end + def create_message(data) object_kind = data[:object_kind] diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index e9e1e276e7..d24aa317cf 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -15,31 +15,17 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # require 'uri' class IrkerService < Service + prop_accessor :server_host, :server_port, :default_irc_uri prop_accessor :colorize_messages, :recipients, :channels validates :recipients, presence: true, if: :activated? - validate :check_recipients_count, if: :activated? before_validation :get_channels - after_initialize :initialize_settings - - # Writer for RSpec tests - attr_writer :settings - - def initialize_settings - # See the documentation (doc/project_services/irker.md) for possible values - # here - @settings ||= { - server_ip: 'localhost', - server_port: 6659, - max_channels: 3, - default_irc_uri: nil - } - end def title 'Irker (IRC gateway)' @@ -50,20 +36,6 @@ class IrkerService < Service 'gateway.' end - def help - msg = 'Recipients have to be specified with a full URI: '\ - 'irc[s]://irc.network.net[:port]/#channel. Special cases: if you want '\ - 'the channel to be a nickname instead, append ",isnick" to the channel '\ - 'name; if the channel is protected by a secret password, append '\ - '"?key=secretpassword" to the URI.' - - unless @settings[:default_irc].nil? - msg += ' Note that a default IRC URI is provided by this service\'s '\ - "administrator: #{default_irc}. You can thus just give a channel name." - end - msg - end - def to_param 'irker' end @@ -76,31 +48,46 @@ class IrkerService < Service return unless supported_events.include?(data[:object_kind]) IrkerWorker.perform_async(project_id, channels, - colorize_messages, data, @settings) + colorize_messages, data, settings) + end + + def settings + { server_host: server_host.present? ? server_host : 'localhost', + server_port: server_port.present? ? server_port : 6659 + } end def fields [ + { type: 'text', name: 'server_host', placeholder: 'localhost', + help: 'Irker daemon hostname (defaults to localhost)' }, + { type: 'text', name: 'server_port', placeholder: 6659, + help: 'Irker daemon port (defaults to 6659)' }, + { type: 'text', name: 'default_irc_uri', title: 'Default IRC URI', + help: 'A default IRC URI to prepend before each recipient (optional)', + placeholder: 'irc://irc.network.net:6697/' }, { type: 'textarea', name: 'recipients', - placeholder: 'Recipients/channels separated by whitespaces' }, + placeholder: 'Recipients/channels separated by whitespaces', + help: 'Recipients have to be specified with a full URI: '\ + 'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\ + 'you want the channel to be a nickname instead, append ",isnick" to ' \ + 'the channel name; if the channel is protected by a secret password, ' \ + ' append "?key=secretpassword" to the URI. Note that if you specify a ' \ + ' default IRC URI to prepend before each recipient, you can just give ' \ + ' a channel name.' }, { type: 'checkbox', name: 'colorize_messages' }, ] end + def help + ' NOTE: Irker does NOT have built-in authentication, which makes it' \ + ' vulnerable to spamming IRC channels if it is hosted outside of a ' \ + ' firewall. Please make sure you run the daemon within a secured network ' \ + ' to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.' + end + private - def check_recipients_count - return true if recipients.nil? || recipients.empty? - - if recipients.split(/\s+/).count > max_chans - errors.add(:recipients, "are limited to #{max_chans}") - end - end - - def max_chans - @settings[:max_channels] - end - def get_channels return true unless :activated? return true if recipients.nil? || recipients.empty? @@ -113,40 +100,35 @@ class IrkerService < Service def map_recipients self.channels = recipients.split(/\s+/).map do |recipient| - format_channel default_irc_uri, recipient + format_channel(recipient) end channels.reject! &:nil? end - def default_irc_uri - default_irc = @settings[:default_irc_uri] - if !(default_irc.nil? || default_irc[-1] == '/') - default_irc += '/' - end - default_irc - end - - def format_channel(default_irc, recipient) - cnt = 0 - url = nil + def format_channel(recipient) + uri = nil # Try to parse the chan as a full URI begin - uri = URI.parse(recipient) - raise URI::InvalidURIError if uri.scheme.nil? && cnt == 0 + uri = consider_uri(URI.parse(recipient)) rescue URI::InvalidURIError - unless default_irc.nil? - cnt += 1 - recipient = "#{default_irc}#{recipient}" - retry if cnt == 1 - end - else - url = consider_uri uri end - url + + unless uri.present? and default_irc_uri.nil? + begin + new_recipient = URI.join(default_irc_uri, '/', recipient).to_s + uri = consider_uri(URI.parse(new_recipient)) + rescue + Rails.logger.error("Unable to create a valid URL from #{default_irc_uri} and #{recipient}") + end + end + + uri end def consider_uri(uri) + return nil if uri.scheme.nil? + # Authorize both irc://domain.com/#chan and irc://domain.com/chan if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil? # Do not authorize irc://domain.com/ diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index c8ab9d63b7..936e574ccc 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -81,18 +81,13 @@ class IssueTrackerService < Service result = false begin - url = URI.parse(self.project_url) + response = HTTParty.head(self.project_url, verify: true) - if url.host && url.port - http = Net::HTTP.start(url.host, url.port, { open_timeout: 5, read_timeout: 5 }) - response = http.head("/") - - if response - message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}" - result = true - end + if response + message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}" + result = true end - rescue Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED => error + rescue HTTParty::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED => error message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}" end Rails.logger.info(message) diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index fcd9dc2f33..bfa8fc7b86 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -24,13 +24,12 @@ class JiraService < IssueTrackerService prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def help - issue_tracker_link = help_page_path("integration", "external-issue-tracker") + line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ + 'allow a user to easily navigate to the Jira issue tracker. See the '\ + '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\ + 'for details.' - line1 = "Setting `project_url`, `issues_url` and `new_issue_url` will "\ - "allow a user to easily navigate to the Jira issue tracker. "\ - "See the [integration doc](#{issue_tracker_link}) for details." - - line2 = 'Support for referencing commits and automatic closing of Jira issues directly ' \ + line2 = 'Support for referencing commits and automatic closing of Jira issues directly '\ 'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)' [line1, line2].join("\n\n") diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 772c868d9c..231973fa54 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -2,7 +2,7 @@ class ProjectWiki include Gitlab::ShellAdapter MARKUPS = { - 'Markdown' => :markdown, + 'Markdown' => :md, 'RDoc' => :rdoc, 'AsciiDoc' => :asciidoc } unless defined?(MARKUPS) @@ -112,7 +112,7 @@ class ProjectWiki end def repository - Repository.new(path_with_namespace, default_branch) + Repository.new(path_with_namespace, default_branch, @project) end def default_branch diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 97207ba127..8ebd790a89 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -18,6 +18,6 @@ class ProtectedBranch < ActiveRecord::Base validates :project, presence: true def commit - project.repository.commit(self.name) + project.commit(self.name) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 263a436d52..24c32d9005 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,11 +1,17 @@ class Repository include Gitlab::ShellAdapter - attr_accessor :raw_repository, :path_with_namespace + attr_accessor :raw_repository, :path_with_namespace, :project - def initialize(path_with_namespace, default_branch = nil) + def initialize(path_with_namespace, default_branch = nil, project = nil) @path_with_namespace = path_with_namespace - @raw_repository = Gitlab::Git::Repository.new(path_to_repo) if path_with_namespace + @project = project + + if path_with_namespace + @raw_repository = Gitlab::Git::Repository.new(path_to_repo) + @raw_repository.autocrlf = :input + end + rescue Gitlab::Git::Repository::NoRepository nil end @@ -28,7 +34,7 @@ class Repository def commit(id = 'HEAD') return nil unless raw_repository commit = Gitlab::Git::Commit.find(raw_repository, id) - commit = Commit.new(commit) if commit + commit = Commit.new(commit, @project) if commit commit rescue Rugged::OdbError nil @@ -42,13 +48,13 @@ class Repository limit: limit, offset: offset, ) - commits = Commit.decorate(commits) if commits.present? + commits = Commit.decorate(commits, @project) if commits.present? commits end def commits_between(from, to) commits = Gitlab::Git::Commit.between(raw_repository, from, to) - commits = Commit.decorate(commits) if commits.present? + commits = Commit.decorate(commits, @project) if commits.present? commits end @@ -88,18 +94,6 @@ class Repository gitlab_shell.rm_tag(path_with_namespace, tag_name) end - def round_commit_count - if commit_count > 10000 - '10000+' - elsif commit_count > 5000 - '5000+' - elsif commit_count > 1000 - '1000+' - else - commit_count - end - end - def branch_names cache.fetch(:branch_names) { raw_repository.branch_names } end @@ -124,28 +118,29 @@ class Repository cache.fetch(:size) { raw_repository.size } end + def cache_keys + %i(size branch_names tag_names commit_count + readme version contribution_guide changelog license) + end + + def build_cache + cache_keys.each do |key| + unless cache.exist?(key) + send(key) + end + end + end + def expire_cache - %i(size branch_names tag_names commit_count graph_log - readme version contribution_guide changelog license).each do |key| + cache_keys.each do |key| cache.expire(key) end end - def graph_log - cache.fetch(:graph_log) do - commits = raw_repository.log(limit: 6000, skip_merges: true, - ref: root_ref) - - commits.map do |rugged_commit| - commit = Gitlab::Git::Commit.new(rugged_commit) - - { - author_name: commit.author_name, - author_email: commit.author_email, - additions: commit.stats.additions, - deletions: commit.stats.deletions, - } - end + def rebuild_cache + cache_keys.each do |key| + cache.expire(key) + send(key) end end @@ -153,6 +148,10 @@ class Repository @lookup_cache ||= {} end + def expire_branch_names + cache.expire(:branch_names) + end + def method_missing(m, *args, &block) if m == :lookup && !block_given? lookup_cache[m] ||= {} @@ -162,14 +161,14 @@ class Repository end end - def respond_to?(method) - return true if raw_repository.respond_to?(method) - - super + def respond_to_missing?(method, include_private = false) + raw_repository.respond_to?(method, include_private) || super end def blob_at(sha, path) - Gitlab::Git::Blob.find(self, sha, path) + unless Gitlab::Git.blank_ref?(sha) + Gitlab::Git::Blob.find(self, sha, path) + end end def blob_by_oid(oid) @@ -369,6 +368,50 @@ class Repository @root_ref ||= raw_repository.root_ref end + def merged_to_root_ref?(branch_name) + branch_commit = commit(branch_name) + root_ref_commit = commit(root_ref) + + if branch_commit + rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id + else + nil + end + end + + def search_files(query, ref) + offset = 2 + args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref}) + Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) + end + + def parse_search_result(result) + ref = nil + filename = nil + startline = 0 + + result.each_line.each_with_index do |line, index| + if line =~ /^.*:.*:\d+:/ + ref, filename, startline = line.split(':') + startline = startline.to_i - index + break + end + end + + data = "" + + result.each_line do |line| + data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '') + end + + OpenStruct.new( + filename: filename, + ref: ref, + startline: startline, + data: data + ) + end + private def cache diff --git a/app/models/security_event.rb b/app/models/security_event.rb new file mode 100644 index 0000000000..68c00adad5 --- /dev/null +++ b/app/models/security_event.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: audit_events +# +# id :integer not null, primary key +# author_id :integer not null +# type :string(255) not null +# entity_id :integer not null +# entity_type :string(255) not null +# details :text +# created_at :datetime +# updated_at :datetime +# + +class SecurityEvent < AuditEvent +end diff --git a/app/models/service.rb b/app/models/service.rb index 393cf55a69..dcef2866c3 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # # To add new service you should build a class inherited from Service @@ -86,10 +87,16 @@ class Service < ActiveRecord::Base %w(push tag_push issue merge_request) end - def execute + def execute(data) # implement inside child end + def test(data) + # default implementation + result = execute(data) + { success: result.present?, result: result } + end + def can_test? !project.empty_repo? end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b35e72c4bd..b0831982aa 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -16,13 +16,16 @@ # class Snippet < ActiveRecord::Base - include Sortable - include Linguist::BlobHelper include Gitlab::VisibilityLevel + include Linguist::BlobHelper + include Participable + include Referable + include Sortable default_value_for :visibility_level, Snippet::PRIVATE - belongs_to :author, class_name: "User" + belongs_to :author, class_name: 'User' + belongs_to :project has_many :notes, as: :noteable, dependent: :destroy @@ -31,7 +34,6 @@ class Snippet < ActiveRecord::Base validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } validates :file_name, - presence: true, length: { within: 0..255 }, format: { with: Gitlab::Regex.file_name_regex, message: Gitlab::Regex.file_name_regex_message } @@ -47,6 +49,32 @@ class Snippet < ActiveRecord::Base scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } + participant :author, :notes + + def self.reference_prefix + '$' + end + + # Pattern used to extract `$123` snippet references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{id}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference + end + def self.content_types [ ".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java", diff --git a/app/models/tree.rb b/app/models/tree.rb index f279e896cd..93b3246a66 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -1,11 +1,11 @@ class Tree - include Gitlab::MarkdownHelper + include Gitlab::MarkupHelper attr_accessor :repository, :sha, :path, :entries def initialize(repository, sha, path = '/') path = '/' if path.blank? - + @repository = repository @sha = sha @path = path @@ -20,7 +20,7 @@ class Tree available_readmes = blobs.select(&:readme?) if available_readmes.count == 0 - return @readme = nil + return @readme = nil end # Take the first previewable readme, or the first available readme, if we diff --git a/app/models/user.rb b/app/models/user.rb index d6b93afe73..57145cc6b6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -49,18 +49,28 @@ # password_automatically_set :boolean default(FALSE) # bitbucket_access_token :string(255) # bitbucket_access_token_secret :string(255) +# location :string(255) +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean default(FALSE), not null +# otp_backup_codes :text # public_email :string(255) default(""), not null +# dashboard :integer default(0) +# project_view :integer default(0) # require 'carrierwave/orm/activerecord' require 'file_size_validator' class User < ActiveRecord::Base - include Sortable - include Gitlab::ConfigHelper - include TokenAuthenticatable extend Gitlab::ConfigHelper + + include Gitlab::ConfigHelper include Gitlab::CurrentSettings + include Referable + include Sortable + include TokenAuthenticatable default_value_for :admin, false default_value_for :can_create_group, gitlab_config.default_can_create_group @@ -69,8 +79,15 @@ class User < ActiveRecord::Base default_value_for :hide_no_password, false default_value_for :theme_id, gitlab_config.default_theme - devise :database_authenticatable, :lockable, :async, - :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable + devise :two_factor_authenticatable, + otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp + alias_attribute :two_factor_enabled, :otp_required_for_login + + devise :two_factor_backupable, otp_number_of_backup_codes: 10 + serialize :otp_backup_codes, JSON + + devise :lockable, :async, :recoverable, :rememberable, :trackable, + :validatable, :omniauthable, :confirmable, :registerable attr_accessor :force_random_password @@ -122,7 +139,9 @@ class User < ActiveRecord::Base # Validations # validates :name, presence: true - validates :email, presence: true, email: { strict_mode: true }, uniqueness: true + # Note that a 'uniqueness' and presence check is provided by devise :validatable for email. We do not need to + # duplicate that here as the validation framework will have duplicate errors in the event of a failure. + validates :email, presence: true, email: { strict_mode: true } validates :notification_email, presence: true, email: { strict_mode: true } validates :public_email, presence: true, email: { strict_mode: true }, allow_blank: true, uniqueness: true validates :bio, length: { maximum: 255 }, allow_blank: true @@ -136,22 +155,32 @@ class User < ActiveRecord::Base validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } - validate :avatar_type, if: ->(user) { user.avatar_changed? } + validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } + validate :owns_public_email, if: ->(user) { user.public_email_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :generate_password, on: :create + before_validation :restricted_signup_domains, on: :create before_validation :sanitize_attrs before_validation :set_notification_email, if: ->(user) { user.email_changed? } before_validation :set_public_email, if: ->(user) { user.public_email_changed? } + after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } before_save :ensure_authentication_token after_save :ensure_namespace_correct after_initialize :set_projects_limit after_create :post_create_hook after_destroy :post_destroy_hook + # User's Dashboard preference + # Note: When adding an option, it MUST go on the end of the array. + enum dashboard: [:projects, :stars] + + # User's Project preference + # Note: When adding an option, it MUST go on the end of the array. + enum project_view: [:readme, :activity] alias_attribute :private_token, :authentication_token @@ -170,11 +199,13 @@ class User < ActiveRecord::Base mount_uploader :avatar, AvatarUploader # Scopes - scope :admins, -> { where(admin: true) } + scope :admins, -> { where(admin: true) } scope :blocked, -> { with_state(:blocked) } scope :active, -> { with_state(:active) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } + scope :with_two_factor, -> { where(two_factor_enabled: true) } + scope :without_two_factor, -> { where(two_factor_enabled: false) } # # Class methods @@ -199,18 +230,37 @@ class User < ActiveRecord::Base end end - def find_for_commit(email, name) - # Prefer email match over name match - User.where(email: email).first || - User.joins(:emails).where(emails: { email: email }).first || - User.where(name: name).first + # Find a User by their primary email or any associated secondary email + def find_by_any_email(email) + user_table = arel_table + email_table = Email.arel_table + + # Use ARel to build a query: + query = user_table. + # SELECT "users".* FROM "users" + project(user_table[Arel.star]). + # LEFT OUTER JOIN "emails" + join(email_table, Arel::Nodes::OuterJoin). + # ON "users"."id" = "emails"."user_id" + on(user_table[:id].eq(email_table[:user_id])). + # WHERE ("user"."email" = '' OR "emails"."email" = '') + where(user_table[:email].eq(email).or(email_table[:email].eq(email))) + + find_by_sql(query.to_sql).first end def filter(filter_name) case filter_name - when "admins"; self.admins - when "blocked"; self.blocked - when "wop"; self.without_projects + when 'admins' + self.admins + when 'blocked' + self.blocked + when 'two_factor_disabled' + self.without_two_factor + when 'two_factor_enabled' + self.with_two_factor + when 'wop' + self.without_projects else self.active end @@ -225,6 +275,10 @@ class User < ActiveRecord::Base value: login.to_s.downcase).first end + def find_by_username!(username) + find_by!('lower(username) = ?', username.downcase) + end + def by_username_or_id(name_or_id) where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first end @@ -232,6 +286,18 @@ class User < ActiveRecord::Base def build_user(attrs = {}) User.new(attrs) end + + def reference_prefix + '@' + end + + # Pattern used to extract `@user` user references from text + def reference_pattern + %r{ + #{Regexp.escape(reference_prefix)} + (?#{Gitlab::Regex::NAMESPACE_REGEX_STR}) + }x + end end # @@ -242,6 +308,10 @@ class User < ActiveRecord::Base username end + def to_reference(_from_project = nil) + "#{self.class.reference_prefix}#{username}" + end + def notification @notification ||= Notification.new(self) end @@ -261,6 +331,16 @@ class User < ActiveRecord::Base @reset_token end + def disable_two_factor! + update_attributes( + two_factor_enabled: false, + encrypted_otp_secret: nil, + encrypted_otp_secret_iv: nil, + encrypted_otp_secret_salt: nil, + otp_backup_codes: nil + ) + end + def namespace_uniq namespace_name = self.username existing_namespace = Namespace.by_path(namespace_name) @@ -276,13 +356,31 @@ class User < ActiveRecord::Base end def unique_email - self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email) + if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email) + self.errors.add(:email, 'has already been taken') + end end def owns_notification_email self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) end + def owns_public_email + return if self.public_email.blank? + + self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email) + end + + def update_emails_with_primary_email + primary_email_record = self.emails.find_by(email: self.email) + if primary_email_record + primary_email_record.destroy + self.emails.create(email: self.email_was) + + self.update_secondary_emails! + end + end + # Groups user has access to def authorized_groups @authorized_groups ||= begin @@ -303,9 +401,11 @@ class User < ActiveRecord::Base end def owned_projects - @owned_projects ||= begin - Project.where(namespace_id: owned_groups.pluck(:id).push(namespace.id)).joins(:namespace) - end + @owned_projects ||= + begin + namespace_ids = owned_groups.pluck(:id).push(namespace.id) + Project.in_namespace(namespace_ids).joins(:namespace) + end end # Team membership in authorized projects @@ -418,7 +518,7 @@ class User < ActiveRecord::Base end def project_deploy_keys - DeployKey.in_projects(self.authorized_projects.pluck(:id)) + DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id) end def accessible_deploy_keys @@ -434,7 +534,7 @@ class User < ActiveRecord::Base end def sanitize_attrs - %w(name username skype linkedin twitter bio).each do |attr| + %w(name username skype linkedin twitter).each do |attr| value = self.send(attr) self.send("#{attr}=", Sanitize.clean(value)) if value.present? end @@ -452,6 +552,12 @@ class User < ActiveRecord::Base end end + def update_secondary_emails! + self.set_notification_email + self.set_public_email + self.save if self.notification_email_changed? || self.public_email_changed? + end + def set_projects_limit connection_default_value_defined = new_record? && !projects_limit_changed? return unless self.projects_limit.nil? || connection_default_value_defined @@ -513,7 +619,7 @@ class User < ActiveRecord::Base end def all_ssh_keys - keys.map(&:key) + keys.map(&:publishable_key) end def temp_oauth_email? @@ -600,6 +706,12 @@ class User < ActiveRecord::Base end end + def namespaces + namespace_ids = groups.pluck(:id) + namespace_ids.push(namespace.id) + Namespace.where(id: namespace_ids) + end + def oauth_authorized_tokens Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) end @@ -611,4 +723,31 @@ class User < ActiveRecord::Base select(:project_id). uniq.map(&:project_id) end + + def restricted_signup_domains + email_domains = current_application_settings.restricted_signup_domains + + unless email_domains.blank? + match_found = email_domains.any? do |domain| + escaped = Regexp.escape(domain).gsub('\*','.*?') + regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE + email_domain = Mail::Address.new(self.email).domain + email_domain =~ regexp + end + + unless match_found + self.errors.add :email, + 'is not whitelisted. ' + + 'Email domains valid for registration are: ' + + email_domains.join(', ') + return false + end + end + + true + end + + def can_be_removed? + !solo_owned_groups.present? + end end diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb new file mode 100644 index 0000000000..a7f090655e --- /dev/null +++ b/app/services/audit_event_service.rb @@ -0,0 +1,25 @@ +class AuditEventService + def initialize(author, entity, details = {}) + @author, @entity, @details = author, entity, details + end + + def for_authentication + @details = { + with: @details[:with], + target_id: @author.id, + target_type: "User", + target_details: @author.name, + } + + self + end + + def security_event + SecurityEvent.create( + author_id: @author.id, + entity_id: @entity.id, + entity_type: @entity.class.name, + details: @details + ) + end +end diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 25f9e20324..1a7318048b 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -38,7 +38,7 @@ class CreateTagService < BaseService end def create_push_data(project, user, tag) - commits = [project.repository.commit(tag.target)].compact + commits = [project.commit(tag.target)].compact Gitlab::PushDataBuilder. build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", commits, tag.message) end diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb new file mode 100644 index 0000000000..e622fd5ea5 --- /dev/null +++ b/app/services/delete_user_service.rb @@ -0,0 +1,22 @@ +class DeleteUserService + attr_accessor :current_user + + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + if user.solo_owned_groups.present? + user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' + user + else + user.personal_projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + + user.destroy + end + end +end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb new file mode 100644 index 0000000000..d929a67629 --- /dev/null +++ b/app/services/destroy_group_service.rb @@ -0,0 +1,17 @@ +class DestroyGroupService + attr_accessor :group, :current_user + + def initialize(group, user) + @group, @current_user = group, user + end + + def execute + @group.projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + + @group.destroy + end +end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 31e0167d24..5a2c97b08a 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -21,7 +21,6 @@ class GitPushService project.ensure_satellite_exists project.repository.expire_cache - project.update_repository_size if push_remove_branch?(ref, newrev) @push_commits = [] @@ -31,6 +30,10 @@ class GitPushService # Initial push to the default branch. Take the full history of that branch as "newly pushed". @push_commits = project.repository.commits(newrev) + # Ensure HEAD points to the default branch in case it is not master + branch_name = Gitlab::Git.ref_name(ref) + project.change_head(branch_name) + # Set protection on the default branch if configured if (current_application_settings.default_branch_protection != PROTECTION_NONE) developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false @@ -57,6 +60,7 @@ class GitPushService EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks) + ProjectCacheWorker.perform_async(project.id) end protected @@ -70,7 +74,7 @@ class GitPushService # Close issues if these commits were pushed to the project's default branch and the commit message matches the # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to # a different branch. - issues_to_close = commit.closes_issues(project, user) + issues_to_close = commit.closes_issues(user) # Load commit author only if needed. # For push with 1k commits it prevents 900+ requests in database @@ -84,18 +88,24 @@ class GitPushService end end - # Create cross-reference notes for any other references. Omit any issues that were referenced in an - # issue-closing phrase, or have already been mentioned from this commit (probably from this commit - # being pushed to a different branch). - refs = commit.references(project, user) - issues_to_close - refs.reject! { |r| commit.has_mentioned?(r) } + if project.default_issues_tracker? + create_cross_reference_notes(commit, issues_to_close) + end + end + end - if refs.present? - author ||= commit_user(commit) + def create_cross_reference_notes(commit, issues_to_close) + # Create cross-reference notes for any other references. Omit any issues that were referenced in an + # issue-closing phrase, or have already been mentioned from this commit (probably from this commit + # being pushed to a different branch). + refs = commit.references(project, user) - issues_to_close + refs.reject! { |r| commit.has_mentioned?(r) } - refs.each do |r| - Note.create_cross_reference_note(r, commit, author, project) - end + if refs.present? + author ||= commit_user(commit) + + refs.each do |r| + SystemNoteService.cross_reference(r, commit, author) end end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index bf203bbd69..1cc42b0b0a 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -2,15 +2,15 @@ class GitTagPushService attr_accessor :project, :user, :push_data def execute(project, user, oldrev, newrev, ref) - @project, @user = project, user + project.repository.expire_cache + @project, @user = project, user @push_data = build_push_data(oldrev, newrev, ref) EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) - - project.repository.expire_cache + ProjectCacheWorker.perform_async(project.id) true end @@ -25,7 +25,7 @@ class GitTagPushService tag_name = Gitlab::Git.ref_name(ref) tag = project.repository.find_tag(tag_name) if tag && tag.target == newrev - commit = project.repository.commit(tag.target) + commit = project.commit(tag.target) commits = [commit].compact message = tag.message end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 5e1906ad2a..15b3825f96 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -2,17 +2,38 @@ class IssuableBaseService < BaseService private def create_assignee_note(issuable) - Note.create_assignee_change_note( + SystemNoteService.change_assignee( issuable, issuable.project, current_user, issuable.assignee) end def create_milestone_note(issuable) - Note.create_milestone_change_note( + SystemNoteService.change_milestone( issuable, issuable.project, current_user, issuable.milestone) end def create_labels_note(issuable, added_labels, removed_labels) - Note.create_labels_change_note( + SystemNoteService.change_label( issuable, issuable.project, current_user, added_labels, removed_labels) end + + def create_title_change_note(issuable, old_title) + SystemNoteService.change_title( + issuable, issuable.project, current_user, old_title) + end + + def create_branch_change_note(issuable, branch_type, old_branch, new_branch) + SystemNoteService.change_branch( + issuable, issuable.project, current_user, branch_type, + old_branch, new_branch) + end + + def filter_params(issuable_ability_name = :issue) + ability = :"admin_#{issuable_ability_name}" + + unless can?(current_user, ability, project) + params.delete(:milestone_id) + params.delete(:label_ids) + params.delete(:assignee_id) + end + end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index c3ca04a434..770f32de94 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -10,6 +10,10 @@ module Issues private + def filter_params + super(:issue) + end + def execute_hooks(issue, action = 'open') issue_data = hook_data(issue, action) issue.project.execute_hooks(issue_data, :issue_hooks) diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index eb07413ee9..de8387c490 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -10,7 +10,7 @@ module Issues issues = Issue.where(id: issues_ids) issues.each do |issue| - next unless can?(current_user, :modify_issue, issue) + next unless can?(current_user, :update_issue, issue) Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index f670019cc6..3d85f97b7e 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -1,7 +1,7 @@ module Issues class CloseService < Issues::BaseService def execute(issue, commit = nil) - if issue.close + if project.default_issues_tracker? && issue.close event_service.close_issue(issue, current_user) create_note(issue, commit) notification_service.close_issue(issue, current_user) @@ -14,7 +14,7 @@ module Issues private def create_note(issue, current_commit) - Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit) + SystemNoteService.change_status(issue, issue.project, current_user, issue.state, current_commit) end end end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index d5c17906a5..1ea4b72216 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -1,6 +1,7 @@ module Issues class CreateService < Issues::BaseService def execute + filter_params label_params = params[:label_ids] issue = project.issues.new(params.except(:label_ids)) issue.author = current_user diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb index 1e5c398516..e48ca359f4 100644 --- a/app/services/issues/reopen_service.rb +++ b/app/services/issues/reopen_service.rb @@ -14,7 +14,7 @@ module Issues private def create_note(issue) - Note.create_status_change_note(issue, issue.project, current_user, issue.state, nil) + SystemNoteService.change_status(issue, issue.project, current_user, issue.state, nil) end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 8f04a69287..2fc6ef7f35 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -1,26 +1,20 @@ module Issues class UpdateService < Issues::BaseService def execute(issue) - state = params[:state_event] - - case state + case params.delete(:state_event) when 'reopen' Issues::ReopenService.new(project, current_user, {}).execute(issue) when 'close' Issues::CloseService.new(project, current_user, {}).execute(issue) - when 'task_check' - issue.update_nth_task(params[:task_num].to_i, true) - when 'task_uncheck' - issue.update_nth_task(params[:task_num].to_i, false) end params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE + filter_params old_labels = issue.labels.to_a - if params.present? && issue.update_attributes(params.except(:state_event, - :task_num)) + if params.present? && issue.update_attributes(params.merge(updated_by: current_user)) issue.reset_events_cache if issue.labels != old_labels @@ -37,7 +31,11 @@ module Issues notification_service.reassigned_issue(issue, current_user) end - issue.notice_added_references(issue.project, current_user) + if issue.previous_changes.include?('title') + create_title_change_note(issue, issue.previous_changes['title'].first) + end + + issue.create_new_cross_references!(issue.project, current_user) execute_hooks(issue, 'update') end diff --git a/app/services/merge_requests/auto_merge_service.rb b/app/services/merge_requests/auto_merge_service.rb index 378b39bb9d..cdedf48b0c 100644 --- a/app/services/merge_requests/auto_merge_service.rb +++ b/app/services/merge_requests/auto_merge_service.rb @@ -14,7 +14,7 @@ module MergeRequests create_merge_event(merge_request, current_user) create_note(merge_request) notification_service.merge_mr(merge_request, current_user) - execute_hooks(merge_request) + execute_hooks(merge_request, 'merge') true else diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index f6e1ae6f28..7b306a8a53 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -2,7 +2,7 @@ module MergeRequests class BaseService < ::IssuableBaseService def create_note(merge_request) - Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) + SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil) end def hook_data(merge_request, action) @@ -20,5 +20,11 @@ module MergeRequests merge_request.project.execute_services(merge_data, :merge_request_hooks) end end + + private + + def filter_params + super(:merge_request) + end end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index a44b91166e..956480938c 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -29,7 +29,7 @@ module MergeRequests # At this point we decide if merge request can be created # If we have at least one commit to merge -> creation allowed if commits.present? - merge_request.compare_commits = Commit.decorate(commits) + merge_request.compare_commits = Commit.decorate(commits, merge_request.source_project) merge_request.can_be_created = true merge_request.compare_failed = false diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index ca8d80f6c0..9651b16462 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -1,10 +1,17 @@ module MergeRequests class CreateService < MergeRequests::BaseService def execute + # @project is used to determine whether the user can set the merge request's + # assignee, milestone and labels. Whether they can depends on their + # permissions on the target project. + source_project = @project + @project = Project.find(params[:target_project_id]) if params[:target_project_id] + + filter_params label_params = params[:label_ids] merge_request = MergeRequest.new(params.except(:label_ids)) - merge_request.source_project = project - merge_request.target_project ||= project + merge_request.source_project = source_project + merge_request.target_project ||= source_project merge_request.author = current_user if merge_request.save diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index e9b526d1fb..d0648da049 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -10,6 +10,7 @@ module MergeRequests close_merge_requests reload_merge_requests + execute_mr_web_hooks comment_mr_with_commits true @@ -82,8 +83,23 @@ module MergeRequests mr_commit_ids.include?(commit.id) end - Note.create_new_commits_note(merge_request, merge_request.project, - @current_user, new_commits, existing_commits, @oldrev) + SystemNoteService.add_commits(merge_request, merge_request.project, + @current_user, new_commits, + existing_commits, @oldrev) + end + end + + # Call merge request webhook with update branches + def execute_mr_web_hooks + merge_requests = @project.origin_merge_requests.opened + .where(source_branch: @branch_name) + .to_a + merge_requests += @fork_merge_requests.where(source_branch: @branch_name) + .to_a + merge_requests = filter_merge_requests(merge_requests) + + merge_requests.each do |merge_request| + execute_hooks(merge_request, 'update') end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 23af2656c3..25d79e22e3 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -5,32 +5,26 @@ require_relative 'close_service' module MergeRequests class UpdateService < MergeRequests::BaseService def execute(merge_request) - # We dont allow change of source/target projects + # We don't allow change of source/target projects and source branch # after merge request was created params.except!(:source_project_id) params.except!(:target_project_id) + params.except!(:source_branch) - state = params[:state_event] - - case state + case params.delete(:state_event) when 'reopen' MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request) when 'close' MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request) - when 'task_check' - merge_request.update_nth_task(params[:task_num].to_i, true) - when 'task_uncheck' - merge_request.update_nth_task(params[:task_num].to_i, false) end params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE + filter_params old_labels = merge_request.labels.to_a - if params.present? && merge_request.update_attributes( - params.except(:state_event, :task_num) - ) + if params.present? && merge_request.update_attributes(params.merge(updated_by: current_user)) merge_request.reset_events_cache if merge_request.labels != old_labels @@ -41,6 +35,12 @@ module MergeRequests ) end + if merge_request.previous_changes.include?('target_branch') + create_branch_change_note(merge_request, 'target', + merge_request.previous_changes['target_branch'].first, + merge_request.target_branch) + end + if merge_request.previous_changes.include?('milestone_id') create_milestone_note(merge_request) end @@ -50,7 +50,16 @@ module MergeRequests notification_service.reassigned_merge_request(merge_request, current_user) end - merge_request.notice_added_references(merge_request.project, current_user) + if merge_request.previous_changes.include?('title') + create_title_change_note(merge_request, merge_request.previous_changes['title'].first) + end + + if merge_request.previous_changes.include?('target_branch') || + merge_request.previous_changes.include?('source_branch') + merge_request.mark_as_unchecked + end + + merge_request.create_new_cross_references!(merge_request.project, current_user) execute_hooks(merge_request, 'update') end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index e969061f22..482c044404 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -15,7 +15,7 @@ module Notes # Create a cross-reference note if this Note contains GFM that names an # issue, merge request, or commit. note.references.each do |mentioned| - Note.create_cross_reference_note(mentioned, note.noteable, note.author, note.project) + SystemNoteService.cross_reference(mentioned, note.noteable, note.author) end execute_hooks(note) @@ -31,7 +31,7 @@ module Notes def execute_hooks(note) note_data = hook_data(note) - # TODO: Support Webhooks + note.project.execute_hooks(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks) end end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 63431b8247..c22a9333ef 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -1,23 +1,11 @@ module Notes class UpdateService < BaseService - def execute - note = project.notes.find(params[:note_id]) - note.note = params[:note] - if note.save - notification_service.new_note(note) + def execute(note) + return note unless note.editable? - # Skip system notes, like status changes and cross-references. - unless note.system - event_service.leave_note(note, note.author) + note.update_attributes(params.merge(updated_by: current_user)) - # Create a cross-reference note if this Note contains GFM that - # names an issue, merge request, or commit. - note.references.each do |mentioned| - Note.create_cross_reference_note(mentioned, note.noteable, - note.author, note.project) - end - end - end + note.reset_events_cache note end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index cfed7964c3..3735a13636 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -70,12 +70,6 @@ class NotificationService reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email') end - # When we close a merge request we should send next emails: - # - # * merge_request author if their notification level is not Disabled - # * merge_request assignee if their notification level is not Disabled - # * project team members with notification level higher then Participating - # def close_mr(merge_request, current_user) close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email') end @@ -84,22 +78,8 @@ class NotificationService reopen_resource_email(issue, issue.project, current_user, 'issue_status_changed_email', 'reopened') end - # When we merge a merge request we should send next emails: - # - # * merge_request author if their notification level is not Disabled - # * merge_request assignee if their notification level is not Disabled - # * project team members with notification level higher then Participating - # def merge_mr(merge_request, current_user) - recipients = reject_muted_users([merge_request.author, merge_request.assignee], merge_request.target_project) - recipients = add_subscribed_users(recipients, merge_request) - recipients = reject_unsubscribed_users(recipients, merge_request) - recipients = recipients.concat(project_watchers(merge_request.target_project)).uniq - recipients.delete(current_user) - - recipients.each do |recipient| - mailer.merged_merge_request_email(recipient.id, merge_request.id, current_user.id) - end + close_resource_email(merge_request, merge_request.target_project, current_user, 'merged_merge_request_email') end def reopen_mr(merge_request, current_user) @@ -127,37 +107,27 @@ class NotificationService recipients = [] - if note.commit_id.present? - recipients << note.commit_author - end - # Add all users participating in the thread (author, assignee, comment authors) participants = if target.respond_to?(:participants) - target.participants - elsif target.is_a?(Commit) - author_ids = Note.for_commit_id(target.id).pluck(:author_id).uniq - User.where(id: author_ids) + target.participants(note.author) else note.mentioned_users end recipients = recipients.concat(participants) # Merge project watchers - recipients = recipients.concat(project_watchers(note.project)).compact.uniq + recipients = add_project_watchers(recipients, note.project) # Reject users with Mention notification level, except those mentioned in _this_ note. recipients = reject_mention_users(recipients - note.mentioned_users, note.project) recipients = recipients + note.mentioned_users - # Reject mutes users recipients = reject_muted_users(recipients, note.project) recipients = add_subscribed_users(recipients, note.noteable) - recipients = reject_unsubscribed_users(recipients, note.noteable) - # Reject author recipients.delete(note.author) # build notify method like 'note_commit_email' @@ -294,6 +264,10 @@ class NotificationService users end + def add_project_watchers(recipients, project) + recipients.concat(project_watchers(project)).compact.uniq + end + # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) @@ -366,8 +340,7 @@ class NotificationService end def new_resource_email(target, project, method) - recipients = build_recipients(target, project) - recipients.delete(target.author) + recipients = build_recipients(target, project, target.author) recipients.each do |recipient| mailer.send(method, recipient.id, target.id) @@ -375,8 +348,7 @@ class NotificationService end def close_resource_email(target, project, current_user, method) - recipients = build_recipients(target, project) - recipients.delete(current_user) + recipients = build_recipients(target, project, current_user) recipients.each do |recipient| mailer.send(method, recipient.id, target.id, current_user.id) @@ -385,8 +357,7 @@ class NotificationService def reassign_resource_email(target, project, current_user, method) assignee_id_was = previous_record(target, "assignee_id") - recipients = build_recipients(target, project) - recipients.delete(current_user) + recipients = build_recipients(target, project, current_user) recipients.each do |recipient| mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id) @@ -394,27 +365,25 @@ class NotificationService end def reopen_resource_email(target, project, current_user, method, status) - recipients = build_recipients(target, project) - recipients.delete(current_user) + recipients = build_recipients(target, project, current_user) recipients.each do |recipient| mailer.send(method, recipient.id, target.id, status, current_user.id) end end - def build_recipients(target, project) - recipients = - if target.respond_to?(:participants) - target.participants - else - [target.author, target.assignee] - end + def build_recipients(target, project, current_user) + recipients = target.participants(current_user) - recipients = reject_muted_users(recipients, project) + recipients = add_project_watchers(recipients, project) recipients = reject_mention_users(recipients, project) + recipients = reject_muted_users(recipients, project) + recipients = add_subscribed_users(recipients, target) - recipients = recipients.concat(project_watchers(project)).uniq recipients = reject_unsubscribed_users(recipients, target) + + recipients.delete(current_user) + recipients end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index a7afcf8f64..b35aed005d 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -5,6 +5,8 @@ module Projects end def execute + forked_from_project_id = params.delete(:forked_from_project_id) + @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility @@ -45,10 +47,14 @@ module Projects @project.creator = current_user + if forked_from_project_id + @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) + end + Project.transaction do @project.save - unless @project.import? + if @project.persisted? && !@project.import? unless @project.create_repository raise 'Failed to create repository' end @@ -79,6 +85,8 @@ module Projects @project.create_wiki if @project.wiki_enabled? + @project.build_missing_services + event_service.create_project(@project, current_user) system_hook_service.execute_hooks_for(@project, :create) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 7e1d753b02..403f419ec5 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -1,28 +1,69 @@ module Projects class DestroyService < BaseService + include Gitlab::ShellAdapter + + class DestroyError < StandardError; end + + DELETED_FLAG = '+deleted' + def execute return false unless can?(current_user, :remove_project, project) project.team.truncate project.repository.expire_cache unless project.empty_repo? - if project.destroy - GitlabShellWorker.perform_async( - :remove_repository, - project.path_with_namespace - ) + repo_path = project.path_with_namespace + wiki_path = repo_path + '.wiki' - GitlabShellWorker.perform_async( - :remove_repository, - project.path_with_namespace + ".wiki" - ) + Project.transaction do + project.destroy! - project.satellite.destroy + unless remove_repository(repo_path) + raise_error('Failed to remove project repository. Please try again or contact administrator') + end - log_info("Project \"#{project.name}\" was removed") - system_hook_service.execute_hooks_for(project, :destroy) - true + unless remove_repository(wiki_path) + raise_error('Failed to remove wiki repository. Please try again or contact administrator') + end end + + project.satellite.destroy + log_info("Project \"#{project.name}\" was removed") + system_hook_service.execute_hooks_for(project, :destroy) + true + end + + private + + def remove_repository(path) + # Skip repository removal. We use this flag when remove user or group + return true if params[:skip_repo] == true + + # There is a possibility project does not have repository or wiki + return true unless gitlab_shell.exists?(path + '.git') + + new_path = removal_path(path) + + if gitlab_shell.mv_repository(path, new_path) + log_info("Repository \"#{path}\" moved to \"#{new_path}\"") + GitlabShellWorker.perform_in(5.minutes, :remove_repository, new_path) + else + false + end + end + + def raise_error(message) + raise DestroyError.new(message) + end + + # Build a path for removing repositories + # We use `+` because its not allowed by GitLab so user can not create + # project with name cookies+119+deleted and capture someone stalled repository + # + # gitlab/cookies.git -> gitlab/cookies+119+deleted.git + # + def removal_path(path) + "#{path}+#{project.id}#{DELETED_FLAG}" end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 1e4deb6ed3..50f208b11d 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -1,66 +1,28 @@ module Projects class ForkService < BaseService - include Gitlab::ShellAdapter - def execute - @from_project = @project - - project_params = { - visibility_level: @from_project.visibility_level, - description: @from_project.description, + new_params = { + forked_from_project_id: @project.id, + visibility_level: @project.visibility_level, + description: @project.description, + name: @project.name, + path: @project.path, + namespace_id: @params[:namespace].try(:id) || current_user.namespace.id } - project = Project.new(project_params) - project.name = @from_project.name - project.path = @from_project.path - project.creator = @current_user - if @from_project.avatar.present? && @from_project.avatar.image? - project.avatar = @from_project.avatar + if @project.avatar.present? && @project.avatar.image? + new_params[:avatar] = @project.avatar end - if namespace = @params[:namespace] - project.namespace = namespace - else - project.namespace = @current_user.namespace - end + new_project = CreateService.new(current_user, new_params).execute - unless @current_user.can?(:create_projects, project.namespace) - project.errors.add(:namespace, 'insufficient access rights') - return project - end - - # If the project cannot save, we do not want to trigger the project destroy - # as this can have the side effect of deleting a repo attached to an existing - # project with the same name and namespace - if project.valid? - begin - Project.transaction do - #First save the DB entries as they can be rolled back if the repo fork fails - project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id) - if project.save - project.team << [@current_user, :master, @current_user] - end - - #Now fork the repo - unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path) - raise 'forking failed in gitlab-shell' - end - - project.ensure_satellite_exists - end - - if @from_project.gitlab_ci? - ForkRegistrationWorker.perform_async(@from_project.id, project.id, @current_user.private_token) - end - rescue => ex - project.errors.add(:base, 'Fork transaction failed.') - project.destroy + if new_project.persisted? + if @project.gitlab_ci? + ForkRegistrationWorker.perform_async(@project.id, new_project.id, @current_user.private_token) end - else - project.errors.add(:base, 'Invalid fork destination') end - project + new_project end end end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index ae6260bcda..0004a399f4 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -13,19 +13,19 @@ module Projects end def participants_in(type, id) - users = case type - when "Issue" - issue = project.issues.find_by_iid(id) - issue ? issue.participants(current_user) : [] - when "MergeRequest" - merge_request = project.merge_requests.find_by_iid(id) - merge_request ? merge_request.participants(current_user) : [] - when "Commit" - author_ids = Note.for_commit_id(id).pluck(:author_id).uniq - User.where(id: author_ids) - else - [] - end + target = + case type + when "Issue" + project.issues.find_by_iid(id) + when "MergeRequest" + project.merge_requests.find_by_iid(id) + when "Commit" + project.commit(id) + end + + return [] unless target + + users = target.participants(current_user) sorted(users) end @@ -38,13 +38,13 @@ module Projects def groups current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count - { username: group.path, name: "#{group.name} (#{count})" } + { username: group.path, name: group.name, count: count } end end def all_members count = project.team.members.flatten.count - [{ username: "all", name: "All Project and Group Members (#{count})" }] + [{ username: "all", name: "All Project and Group Members", count: count }] end end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 489e03bd5e..f43c0ef70e 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -11,19 +11,16 @@ module Projects include Gitlab::ShellAdapter class TransferError < StandardError; end - def execute - namespace_id = params[:new_namespace_id] - namespace = Namespace.find_by(id: namespace_id) - - if allowed_transfer?(current_user, project, namespace) - transfer(project, namespace) + def execute(new_namespace) + if allowed_transfer?(current_user, project, new_namespace) + transfer(project, new_namespace) else - project.errors.add(:namespace, 'is invalid') + project.errors.add(:new_namespace, 'is invalid') false end rescue Projects::TransferService::TransferError => ex project.reload - project.errors.add(:namespace_id, ex.message) + project.errors.add(:new_namespace, ex.message) false end diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index 0bcc50c81a..e904cb6c6f 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -9,7 +9,7 @@ module Search def execute group = Group.find_by(id: params[:group_id]) if params[:group_id].present? projects = ProjectsFinder.new.execute(current_user) - projects = projects.where(namespace_id: group.id) if group + projects = projects.in_namespace(group.id) if group project_ids = projects.pluck(:id) Gitlab::SearchResults.new(project_ids, params[:search]) diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index c5d0b08845..60235b6be2 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -7,12 +7,12 @@ class SystemHooksService def execute_hooks(data) SystemHook.all.each do |sh| - async_execute_hook sh, data + async_execute_hook(sh, data, 'system_hooks') end end - def async_execute_hook(hook, data) - Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data) + def async_execute_hook(hook, data, hook_name) + Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data, hook_name) end def build_event_data(model, event) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb new file mode 100644 index 0000000000..8253c1f780 --- /dev/null +++ b/app/services/system_note_service.rb @@ -0,0 +1,319 @@ +# SystemNoteService +# +# Used for creating system notes (e.g., when a user references a merge request +# from an issue, an issue's assignee changes, an issue is closed, etc.) +class SystemNoteService + # Called when commits are added to a Merge Request + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # new_commits - Array of Commits added since last push + # existing_commits - Array of Commits added in a previous push + # oldrev - Optional String SHA of a previous Commit + # + # See new_commit_summary and existing_commit_summary. + # + # Returns the created Note object + def self.add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil) + total_count = new_commits.length + existing_commits.length + commits_text = "#{total_count} commit".pluralize(total_count) + + body = "Added #{commits_text}:\n\n" + body << existing_commit_summary(noteable, existing_commits, oldrev) + body << new_commit_summary(new_commits).join("\n") + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the assignee of a Noteable is changed or removed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # assignee - User being assigned, or nil + # + # Example Note text: + # + # "Assignee removed" + # + # "Reassigned to @rspeicher" + # + # Returns the created Note object + def self.change_assignee(noteable, project, author, assignee) + body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when one or more labels on a Noteable are added and/or removed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # added_labels - Array of Labels added + # removed_labels - Array of Labels removed + # + # Example Note text: + # + # "Added ~1 and removed ~2 ~3 labels" + # + # "Added ~4 label" + # + # "Removed ~5 label" + # + # Returns the created Note object + def self.change_label(noteable, project, author, added_labels, removed_labels) + labels_count = added_labels.count + removed_labels.count + + references = ->(label) { "~#{label.id}" } + added_labels = added_labels.map(&references).join(' ') + removed_labels = removed_labels.map(&references).join(' ') + + body = '' + + if added_labels.present? + body << "added #{added_labels}" + body << ' and ' if removed_labels.present? + end + + if removed_labels.present? + body << "removed #{removed_labels}" + end + + body << ' ' << 'label'.pluralize(labels_count) + body = "#{body.capitalize}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the milestone of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # milestone - Milestone being assigned, or nil + # + # Example Note text: + # + # "Milestone removed" + # + # "Miletone changed to 7.11" + # + # Returns the created Note object + def self.change_milestone(noteable, project, author, milestone) + body = 'Milestone ' + body += milestone.nil? ? 'removed' : "changed to #{milestone.title}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the status of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # status - String status + # source - Mentionable performing the change, or nil + # + # Example Note text: + # + # "Status changed to merged" + # + # "Status changed to closed by bc17db76" + # + # Returns the created Note object + def self.change_status(noteable, project, author, status, source) + body = "Status changed to #{status}" + body += " by #{source.gfm_reference}" if source + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the title of a Noteable is changed + # + # noteable - Noteable object that responds to `title` + # project - Project owning noteable + # author - User performing the change + # old_title - Previous String title + # + # Example Note text: + # + # "Title changed from **Old** to **New**" + # + # Returns the created Note object + def self.change_title(noteable, project, author, old_title) + return unless noteable.respond_to?(:title) + + body = "Title changed from **#{old_title}** to **#{noteable.title}**" + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when a branch in Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # branch_type - 'source' or 'target' + # old_branch - old branch name + # new_branch - new branch nmae + # + # Example Note text: + # + # "Target branch changed from `Old` to `New`" + # + # Returns the created Note object + def self.change_branch(noteable, project, author, branch_type, old_branch, new_branch) + body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when a Mentionable references a Noteable + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # author - User performing the reference + # + # Example Note text: + # + # "mentioned in #1" + # + # "mentioned in !2" + # + # "mentioned in 54f7727c" + # + # See cross_reference_note_content. + # + # Returns the created Note object + def self.cross_reference(noteable, mentioner, author) + return if cross_reference_disallowed?(noteable, mentioner) + + gfm_reference = mentioner.gfm_reference(noteable.project) + + note_options = { + project: noteable.project, + author: author, + note: cross_reference_note_content(gfm_reference) + } + + if noteable.kind_of?(Commit) + note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) + else + note_options.merge!(noteable: noteable) + end + + create_note(note_options) + end + + def self.cross_reference?(note_text) + note_text.start_with?(cross_reference_note_prefix) + end + + # Check if a cross-reference is disallowed + # + # This method prevents adding a "mentioned in !1" note on every single commit + # in a merge request. Additionally, it prevents the creation of references to + # external issues (which would fail). + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # + # Returns Boolean + def self.cross_reference_disallowed?(noteable, mentioner) + return true if noteable.is_a?(ExternalIssue) + return false unless mentioner.is_a?(MergeRequest) + return false unless noteable.is_a?(Commit) + + mentioner.commits.include?(noteable) + end + + # Check if a cross reference to a noteable from a mentioner already exists + # + # This method is used to prevent multiple notes being created for a mention + # when a issue is updated, for example. + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # + # Returns Boolean + def self.cross_reference_exists?(noteable, mentioner) + # Initial scope should be system notes of this noteable type + notes = Note.system.where(noteable_type: noteable.class) + + if noteable.is_a?(Commit) + # Commits have non-integer IDs, so they're stored in `commit_id` + notes = notes.where(commit_id: noteable.id) + else + notes = notes.where(noteable_id: noteable.id) + end + + gfm_reference = mentioner.gfm_reference(noteable.project) + notes = notes.where(note: cross_reference_note_content(gfm_reference)) + + notes.count > 0 + end + + private + + def self.create_note(args = {}) + Note.create(args.merge(system: true)) + end + + def self.cross_reference_note_prefix + 'mentioned in ' + end + + def self.cross_reference_note_content(gfm_reference) + "#{cross_reference_note_prefix}#{gfm_reference}" + end + + # Build an Array of lines detailing each commit added in a merge request + # + # new_commits - Array of new Commit objects + # + # Returns an Array of Strings + def self.new_commit_summary(new_commits) + new_commits.collect do |commit| + "* #{commit.short_id} - #{commit.title}" + end + end + + # Build a single line summarizing existing commits being added in a merge + # request + # + # noteable - MergeRequest object + # existing_commits - Array of existing Commit objects + # oldrev - Optional String SHA of a previous Commit + # + # Examples: + # + # "* ea0f8418...2f4426b7 - 24 commits from branch `master`" + # + # "* ea0f8418..4188f0ea - 15 commits from branch `fork:master`" + # + # "* ea0f8418 - 1 commit from branch `feature`" + # + # Returns a newline-terminated String + def self.existing_commit_summary(noteable, existing_commits, oldrev = nil) + return '' if existing_commits.empty? + + count = existing_commits.size + + commit_ids = if count == 1 + existing_commits.first.short_id + else + if oldrev + "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}" + else + "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" + end + end + + commits_text = "#{count} commit".pluralize(count) + + branch = noteable.target_branch + branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork? + + "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n" + end +end diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb index 21ec2c01cb..e85e58751e 100644 --- a/app/services/test_hook_service.rb +++ b/app/services/test_hook_service.rb @@ -1,6 +1,6 @@ class TestHookService def execute(hook, current_user) data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user) - hook.execute(data) + hook.execute(data, 'push_hooks') end end diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb index 9d181c2d2a..e9328bb732 100644 --- a/app/services/update_snippet_service.rb +++ b/app/services/update_snippet_service.rb @@ -9,9 +9,9 @@ class UpdateSnippetService < BaseService def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] + if new_visibility && new_visibility.to_i != snippet.visibility_level - unless can?(current_user, :change_visibility_level, snippet) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(snippet, new_visibility) return snippet end diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml new file mode 100644 index 0000000000..cffd768400 --- /dev/null +++ b/app/views/abuse_reports/new.html.haml @@ -0,0 +1,24 @@ +- page_title "Report abuse" +%h3.page-title Report abuse +%p Please use this form to report users who create spam issues, comments or behave inappropriately. +%hr += form_for @abuse_report, html: { class: 'form-horizontal'} do |f| + = f.hidden_field :user_id + - if @abuse_report.errors.any? + .alert.alert-danger + - @abuse_report.errors.full_messages.each do |msg| + %p= msg + .form-group + = f.label :user_id, class: 'control-label' + .col-sm-10 + - name = "#{@abuse_report.user.name} (@#{@abuse_report.user.username})" + = text_field_tag :user_name, name, class: "form-control", readonly: true + .form-group + = f.label :message, class: 'control-label' + .col-sm-10 + = f.text_area :message, class: "form-control", rows: 2, required: true + .help-block + Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment. + + .form-actions + = f.submit "Send report", class: "btn btn-create" diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml new file mode 100644 index 0000000000..d3afc658cd --- /dev/null +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -0,0 +1,26 @@ +- reporter = abuse_report.reporter +- user = abuse_report.user +%tr + %td + - if reporter + = link_to reporter.name, reporter + - else + (removed) + %td + = abuse_report.created_at.to_s(:short) + %td + = abuse_report.message + %td + - if user + = link_to user.name, user + - else + (removed) + %td + - if user + = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), + data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr" + + %td + - if user + = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" + = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml new file mode 100644 index 0000000000..2e8746146d --- /dev/null +++ b/app/views/admin/abuse_reports/index.html.haml @@ -0,0 +1,17 @@ +- page_title "Abuse Reports" +%h3.page-title Abuse Reports +%hr +- if @abuse_reports.present? + %table.table + %thead + %tr + %th Reported by + %th Reported at + %th Message + %th User + %th Primary action + %th + = render @abuse_reports + = paginate @abuse_reports +- else + %h4 There are no abuse reports diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 4f3565c67e..b67d2116fa 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -6,19 +6,36 @@ %p= msg %fieldset - %legend Features + %legend Visibility and Access Controls + .form-group + = f.label :default_branch_protection, class: 'control-label col-sm-2' + .col-sm-10 + = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' + .form-group.project-visibility-level-holder + = f.label :default_project_visibility, class: 'control-label col-sm-2' + .col-sm-10 + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project') + .form-group.project-visibility-level-holder + = f.label :default_snippet_visibility, class: 'control-label col-sm-2' + .col-sm-10 + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet') + .form-group + = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - restricted_level_checkboxes('restricted-visibility-help').each do |level| + = level + %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets .form-group .col-sm-offset-2.col-sm-10 .checkbox - = f.label :signup_enabled do - = f.check_box :signup_enabled - Signup enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :signin_enabled do - = f.check_box :signin_enabled - Signin enabled + = f.label :version_check_enabled do + = f.check_box :version_check_enabled + Version check enabled + + %fieldset + %legend Account and Limit Settings .form-group .col-sm-offset-2.col-sm-10 .checkbox @@ -30,40 +47,63 @@ .checkbox = f.label :twitter_sharing_enabled do = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' - %strong Twitter enabled + Twitter enabled %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter - %fieldset - %legend Misc .form-group = f.label :default_projects_limit, class: 'control-label col-sm-2' .col-sm-10 = f.number_field :default_projects_limit, class: 'form-control' .form-group - = f.label :default_branch_protection, class: 'control-label col-sm-2' + = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2' .col-sm-10 - = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' + = f.number_field :max_attachment_size, class: 'form-control' .form-group - = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' + = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2' .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } - - restricted_level_checkboxes('restricted-visibility-help').each do |level| - = level - %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets + = f.number_field :session_expire_delay, class: 'form-control' + %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes .form-group - = f.label :home_page_url, class: 'control-label col-sm-2' + = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :user_oauth_applications do + = f.check_box :user_oauth_applications + Allow users to register any application to use GitLab as an OAuth provider + + %fieldset + %legend Sign-in Restrictions + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :signup_enabled do + = f.check_box :signup_enabled + Sign-up enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :signin_enabled do + = f.check_box :signin_enabled + Sign-in enabled + .form-group + = f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control' + .help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form-group + = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2' .col-sm-10 = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' %span.help-block#home_help_block We will redirect non-logged in users to this page + .form-group + = f.label :after_sign_out_path, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' + %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out .form-group = f.label :sign_in_text, class: 'control-label col-sm-2' .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled - .form-group - = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :max_attachment_size, class: 'form-control' .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 39b66647a5..e9c7ca9d5a 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -1,3 +1,4 @@ -%h3.page-title Application settings +- page_title "Settings" +%h3.page-title Settings %hr = render 'form' diff --git a/app/views/admin/applications/edit.html.haml b/app/views/admin/applications/edit.html.haml index e408ae2f29..c596866bde 100644 --- a/app/views/admin/applications/edit.html.haml +++ b/app/views/admin/applications/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @application.name, "Applications" %h3.page-title Edit application - @url = admin_application_path(@application) -= render 'form', application: @application \ No newline at end of file += render 'form', application: @application diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index d550278710..fc921a966f 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Applications" %h3.page-title System OAuth applications %p.light diff --git a/app/views/admin/applications/new.html.haml b/app/views/admin/applications/new.html.haml index 7c62425f19..6310d89bd6 100644 --- a/app/views/admin/applications/new.html.haml +++ b/app/views/admin/applications/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Application" %h3.page-title New application - @url = admin_applications_path -= render 'form', application: @application \ No newline at end of file += render 'form', application: @application diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 2abe390ce1..0ea2ffeda9 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -1,3 +1,4 @@ +- page_title @application.name, "Applications" %h3.page-title Application: #{@application.name} diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 4ef8e878a7..3a01e11510 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Background Jobs" %h3.page-title Background Jobs %p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index 7e29311bf4..17dffebd36 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Broadcast Messages" %h3.page-title Broadcast Messages %p.light @@ -21,11 +22,11 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.color_field :color, value: "#AA33EE", class: "form-control" + = f.color_field :color, value: "#eb9532", class: "form-control" .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.color_field :font, value: "#224466", class: "form-control" + = f.color_field :font, value: "#FFFFFF", class: "form-control" .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index d1c586328a..3732ff847b 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -56,7 +56,12 @@ %span.light.pull-right = boolean_to_icon Gitlab.config.omniauth.enabled .col-md-4 - %h4 Components + %h4 + Components + - if current_application_settings.version_check_enabled + .pull-right + = version_status_badge + %hr %p GitLab diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 2ae83ab95f..2bf1689cbc 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Deploy Keys" .panel.panel-default .panel-heading Public deploy keys (#{@deploy_keys.count}) @@ -15,11 +16,9 @@ - @deploy_keys.each do |deploy_key| %tr %td - = link_to admin_deploy_key_path(deploy_key) do - %strong= deploy_key.title + %strong= deploy_key.title %td - %span - (#{deploy_key.fingerprint}) + %code.key-fingerprint= deploy_key.fingerprint %td %span.cgray added #{time_ago_with_tooltip(deploy_key.created_at)} diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index c00049424c..5b46b3222a 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Deploy Key" %h3.page-title New public deploy key %hr diff --git a/app/views/admin/deploy_keys/show.html.haml b/app/views/admin/deploy_keys/show.html.haml deleted file mode 100644 index cfa2adf92e..0000000000 --- a/app/views/admin/deploy_keys/show.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -.row - .col-md-4 - .panel.panel-default - .panel-heading - Deploy Key - %ul.well-list - %li - %span.light Title: - %strong= @deploy_key.title - %li - %span.light Created on: - %strong= @deploy_key.created_at.stamp("Aug 21, 2011") - - .panel.panel-default - .panel-heading Projects (#{@deploy_key.deploy_keys_projects.count}) - - if @deploy_key.deploy_keys_projects.any? - %ul.well-list - - @deploy_key.projects.each do |project| - %li - %span - %strong - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - .pull-right - = link_to disable_namespace_project_deploy_key_path(project.namespace, project, @deploy_key), data: { confirm: "Are you sure?" }, method: :put, class: "btn-xs btn btn-remove", title: 'Remove deploy key from project' do - %i.fa.fa-times.fa-inverse - - .col-md-8 - %p - %span.light Fingerprint: - %strong= @deploy_key.fingerprint - %pre.well-pre - = @deploy_key.key - .pull-right - = link_to 'Remove', admin_deploy_key_path(@deploy_key), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 9e7751830a..8de2ba74a7 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -12,8 +12,7 @@ - if @group.new_record? .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 .alert.alert-info = render 'shared/group_tips' .form-actions diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml index 824e51c1cf..eb09a6328e 100644 --- a/app/views/admin/groups/edit.html.haml +++ b/app/views/admin/groups/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @group.name, "Groups" %h3.page-title Edit group: #{@group.name} %hr = render 'form' diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 4c53ff5570..5ce7cdf2f8 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Groups" %h3.page-title Groups (#{@groups.total_count}) = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" @@ -10,7 +11,7 @@ = form_tag admin_groups_path, method: :get, class: 'form-inline' do = hidden_field_tag :sort, @sort .form-group - = text_field_tag :name, params[:name], class: "form-control input-mn-300" + = text_field_tag :name, params[:name], class: "form-control" = button_tag "Search", class: "btn submit btn-primary" .pull-right diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml index f46f45c551..c81ee552ac 100644 --- a/app/views/admin/groups/new.html.haml +++ b/app/views/admin/groups/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Group" %h3.page-title New group %hr = render 'form' diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 14996dcd6a..296497a4cd 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,3 +1,4 @@ +- page_title @group.name, "Groups" %h3.page-title Group: #{@group.name} @@ -50,21 +51,22 @@ = paginate @projects, param_name: 'projects_page', theme: 'gitlab' .col-md-6 - .panel.panel-default - .panel-heading - Add user(s) to the group: - .panel-body.form-holder - %p.light - Read more about project permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + - if can?(current_user, :admin_group_member, @group) + .panel.panel-default + .panel-heading + Add user(s) to the group: + .panel-body.form-holder + %p.light + Read more about project permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" - = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do - %div - = users_select_tag(:user_ids, multiple: true, email_user: true) - %div.prepend-top-10 - = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" - %hr - = button_tag 'Add users to group', class: "btn btn-create" + = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do + %div + = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) + %div.prepend-top-10 + = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" + %hr + = button_tag 'Add users to group', class: "btn btn-create" .panel.panel-default .panel-heading %h3.panel-title @@ -85,7 +87,8 @@ (invited) %span.pull-right.light = member.human_access - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + - if can?(current_user, :destroy_group_member, member) + = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + %i.fa.fa-minus.fa-inverse .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 7a9dc113f2..e74e1e85f4 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -1,3 +1,4 @@ +- page_title "System Hooks" %h3.page-title System hooks diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml new file mode 100644 index 0000000000..3a78855822 --- /dev/null +++ b/app/views/admin/identities/_form.html.haml @@ -0,0 +1,20 @@ += form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f| + - if @identity.errors.any? + #error_explanation + .alert.alert-danger + - @identity.errors.full_messages.each do |msg| + %p= msg + + .form-group + = f.label :provider, class: 'control-label' + .col-sm-10 + - values = Gitlab::OAuth::Provider.providers.map { |name| ["#{Gitlab::OAuth::Provider.label_for(name)} (#{name})", name] } + = f.select :provider, values, { allow_blank: false }, class: 'form-control' + .form-group + = f.label :extern_uid, "Identifier", class: 'control-label' + .col-sm-10 + = f.text_field :extern_uid, class: 'form-control', required: true + + .form-actions + = f.submit 'Save changes', class: "btn btn-save" + diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml new file mode 100644 index 0000000000..7362d904b9 --- /dev/null +++ b/app/views/admin/identities/_identity.html.haml @@ -0,0 +1,12 @@ +%tr + %td + = "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})" + %td + = identity.extern_uid + %td + = link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-xs btn-grouped' do + Edit + = link_to [:admin, @user, identity], method: :delete, + class: 'btn btn-xs btn-danger', + data: { confirm: "Are you sure you want to remove this identity?" } do + Delete diff --git a/app/views/admin/identities/edit.html.haml b/app/views/admin/identities/edit.html.haml new file mode 100644 index 0000000000..515d46b0f2 --- /dev/null +++ b/app/views/admin/identities/edit.html.haml @@ -0,0 +1,6 @@ +- page_title "Edit", @identity.provider, "Identities", @user.name, "Users" +%h3.page-title + Edit identity for #{@user.name} +%hr + += render 'form' diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml new file mode 100644 index 0000000000..ae57e3adc4 --- /dev/null +++ b/app/views/admin/identities/index.html.haml @@ -0,0 +1,13 @@ +- page_title "Identities", @user.name, "Users" += render 'admin/users/head' + +- if @identities.present? + %table.table + %thead + %tr + %th Provider + %th Identifier + %th + = render @identities +- else + %h4 This user has no identities diff --git a/app/views/admin/keys/show.html.haml b/app/views/admin/keys/show.html.haml index 5b23027b3a..9ee77c7739 100644 --- a/app/views/admin/keys/show.html.haml +++ b/app/views/admin/keys/show.html.haml @@ -1 +1,2 @@ +- page_title @key.title, "Keys" = render "profiles/keys/key_details", admin: true diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 384c6ee9af..1484baa78e 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, Gitlab::ProductionLogger, Gitlab::SidekiqLogger] %ul.nav.nav-tabs.log-tabs diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 3bbe10bc27..f43d46356f 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,6 +1,7 @@ +- page_title "Projects" += render 'shared/show_aside' + .row - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left %aside.col-md-3 .admin-filter = form_tag admin_namespaces_projects_path, method: :get, class: '' do diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 78684c692c..5260eadf95 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,3 +1,4 @@ +- page_title @project.name_with_namespace, "Projects" %h3.page-title Project: #{@project.name_with_namespace} = link_to edit_project_path(@project), class: "btn pull-right" do @@ -91,8 +92,7 @@ = namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large' .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 = f.submit 'Transfer', class: 'btn btn-primary' .col-md-6 diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index eb7a099bfe..cdbfc60f9a 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -3,92 +3,8 @@ %p #{@service.description} template -= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |f| - - if @service.errors.any? - #error_explanation - .alert.alert-danger - - @service.errors.full_messages.each do |msg| - %p= msg - - if @service.help.present? - .well - = preserve do - = markdown @service.help - - .form-group - = f.label :active, "Active", class: "control-label" - .col-sm-10 - = f.check_box :active - - - if @service.supported_events.length > 1 - .form-group - = f.label :url, "Trigger", class: 'control-label' - .col-sm-10 - - if @service.supported_events.include?("push") - %div - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This url will be triggered by a push to the repository - - if @service.supported_events.include?("tag_push") - %div - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - - if @service.supported_events.include?("note") - %div - = f.check_box :note_events, class: 'pull-left' - .prepend-left-20 - = f.label :note_events, class: 'list-label' do - %strong Comments - %p.light - This url will be triggered when someone adds a comment - - if @service.supported_events.include?("issue") - %div - = f.check_box :issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This url will be triggered when an issue is created - - if @service.supported_events.include?("merge_request") - %div - = f.check_box :merge_requests_events, class: 'pull-left' - .prepend-left-20 - = f.label :merge_requests_events, class: 'list-label' do - %strong Merge Request events - %p.light - This url will be triggered when a merge request is created - - - @service.fields.each do |field| - - name = field[:name] - - title = field[:title] || name.humanize - - value = @service.send(name) unless field[:type] == 'password' - - type = field[:type] - - placeholder = field[:placeholder] - - choices = field[:choices] - - default_choice = field[:default_choice] - - help = field[:help] - - .form-group - = f.label name, title, class: "control-label" - .col-sm-10 - - if type == 'text' - = f.text_field name, class: "form-control", placeholder: placeholder - - elsif type == 'textarea' - = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder - - elsif type == 'checkbox' - = f.check_box name - - elsif type == 'select' - = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - - elsif type == 'password' - = f.password_field name, class: 'form-control' - - if help - %span.help-block= help += form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form| + = render 'shared/service_settings', form: form .form-actions - = f.submit 'Save', class: 'btn btn-save' + = form.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml index bcc5832792..53d970e33c 100644 --- a/app/views/admin/services/edit.html.haml +++ b/app/views/admin/services/edit.html.haml @@ -1 +1,2 @@ +- page_title @service.title, "Service Templates" = render 'form' diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml index 0093fb9776..e237729114 100644 --- a/app/views/admin/services/index.html.haml +++ b/app/views/admin/services/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Service Templates" %h3.page-title Service templates %p.light Service template allows you to set default values for project services diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml new file mode 100644 index 0000000000..9d5e934c8b --- /dev/null +++ b/app/views/admin/users/_head.html.haml @@ -0,0 +1,23 @@ +%h3.page-title + = @user.name + - if @user.blocked? + %span.cred (Blocked) + - if @user.admin + %span.cred (Admin) + + .pull-right + = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do + %i.fa.fa-pencil-square-o + Edit +%hr +%ul.nav.nav-tabs + = nav_link(path: 'users#show') do + = link_to "Account", admin_user_path(@user) + = nav_link(path: 'users#groups') do + = link_to "Groups", groups_admin_user_path(@user) + = nav_link(path: 'users#projects') do + = link_to "Projects", projects_admin_user_path(@user) + = nav_link(path: 'users#keys') do + = link_to "SSH keys", keys_admin_user_path(@user) + = nav_link(controller: :identities) do + = link_to "Identities", admin_user_identities_path(@user) diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml index d71d8189c5..a8837d74dd 100644 --- a/app/views/admin/users/edit.html.haml +++ b/app/views/admin/users/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @user.name, "Users" %h3.page-title Edit user: #{@user.name} .back-link diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml new file mode 100644 index 0000000000..dbecb7bbfd --- /dev/null +++ b/app/views/admin/users/groups.html.haml @@ -0,0 +1,19 @@ +- page_title "Groups", @user.name, "Users" += render 'admin/users/head' + +- if @user.group_members.present? + .panel.panel-default + .panel-heading Groups: + %ul.well-list + - @user.group_members.each do |group_member| + - group = group_member.group + %li.group_member + %span{class: ("list-item-name" unless group_member.owner?)} + %strong= link_to group.name, admin_group_path(group) + .pull-right + %span.light= group_member.human_access + - unless group_member.owner? + = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + %i.fa.fa-times.fa-inverse +- else + .nothing-here-block This user has no groups. diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 25c1730ef7..5e40d95d1c 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,6 +1,7 @@ +- page_title "Users" += render 'shared/show_aside' + .row - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left %aside.col-md-3 .admin-filter %ul.nav.nav-pills.nav-stacked @@ -12,6 +13,14 @@ = link_to admin_users_path(filter: "admins") do Admins %small.pull-right= User.admins.count + %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} + = link_to admin_users_path(filter: 'two_factor_enabled') do + 2FA Enabled + %small.pull-right= User.with_two_factor.count + %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} + = link_to admin_users_path(filter: 'two_factor_disabled') do + 2FA Disabled + %small.pull-right= User.without_two_factor.count %li{class: "#{'active' if params[:filter] == "blocked"}"} = link_to admin_users_path(filter: "blocked") do Blocked @@ -24,6 +33,7 @@ = form_tag admin_users_path, method: :get, class: 'form-inline' do .form-group = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control' + = hidden_field_tag "filter", params[:filter] = button_tag class: 'btn btn-primary' do %i.fa.fa-search %hr @@ -78,11 +88,14 @@ %i.fa.fa-envelope = mail_to user.email, user.email, class: 'light'   - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-sm" + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs" - unless user == current_user - if user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-sm success" + = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success" - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-remove" - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-sm btn-remove" + = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning" + - if user.access_locked? + = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' } + - if user.can_be_removed? + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" = paginate @users, theme: "gitlab" diff --git a/app/views/admin/users/keys.html.haml b/app/views/admin/users/keys.html.haml new file mode 100644 index 0000000000..0711071708 --- /dev/null +++ b/app/views/admin/users/keys.html.haml @@ -0,0 +1,3 @@ +- page_title "Keys", @user.name, "Users" += render 'admin/users/head' += render 'profiles/keys/key_table', admin: true diff --git a/app/views/admin/users/new.html.haml b/app/views/admin/users/new.html.haml index 8fbb757f42..bfc36ed737 100644 --- a/app/views/admin/users/new.html.haml +++ b/app/views/admin/users/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New User" %h3.page-title New user %hr diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml new file mode 100644 index 0000000000..0d7a1a25a8 --- /dev/null +++ b/app/views/admin/users/projects.html.haml @@ -0,0 +1,43 @@ +- page_title "Projects", @user.name, "Users" += render 'admin/users/head' + +- if @user.groups.any? + .panel.panel-default + .panel-heading Group projects + %ul.well-list + - @user.groups.each do |group| + %li + %strong= group.name + – access to + #{pluralize(group.projects.count, 'project')} + +.row + .col-md-6 + - if @personal_projects.present? + = render 'users/projects', projects: @personal_projects + - else + .nothing-here-block This user has no personal projects. + + + .col-md-6 + .panel.panel-default + .panel-heading Joined projects (#{@joined_projects.count}) + %ul.well-list + - @joined_projects.sort_by(&:name_with_namespace).each do |project| + - member = project.team.find_member(@user.id) + %li.project_member + .list-item-name + = link_to admin_namespace_project_path(project.namespace, project), class: dom_class(project) do + = project.name_with_namespace + + - if member + .pull-right + - if member.owner? + %span.light Owner + - else + %span.light= member.human_access + + - if member.respond_to? :project + = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do + %i.fa.fa-times + diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 3524f04c5e..a383ea5738 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -1,227 +1,173 @@ -%h3.page-title - User: - = @user.name - - if @user.blocked? - %span.cred (Blocked) - - if @user.admin - %span.cred (Admin) +- page_title @user.name, "Users" += render 'admin/users/head' - .pull-right - = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o - Edit -%hr -%ul.nav.nav-tabs - %li.active - %a{"data-toggle" => "tab", href: "#account"} Account - %li - %a{"data-toggle" => "tab", href: "#profile"} Profile - %li - %a{"data-toggle" => "tab", href: "#groups"} Groups - %li - %a{"data-toggle" => "tab", href: "#projects"} Projects - %li - %a{"data-toggle" => "tab", href: "#ssh-keys"} SSH keys +.row + .col-md-6 + .panel.panel-default + .panel-heading + = @user.name + %ul.well-list + %li + = image_tag avatar_icon(@user.email, 60), class: "avatar s60" + %li + %span.light Profile page: + %strong + = link_to user_path(@user) do + = @user.username + = render 'users/profile', user: @user -.tab-content - #account.tab-pane.active - .row - .col-md-6 - .panel.panel-default - .panel-heading - Account: - %ul.well-list - %li - %span.light Name: - %strong= @user.name - %li - %span.light Username: - %strong - = @user.username - %li - %span.light Email: - %strong - = mail_to @user.email - - @user.emails.each do |email| - %li - %span.light Secondary email: - %strong= email.email - = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do - %i.fa.fa-times + .panel.panel-default + .panel-heading + Account: + %ul.well-list + %li + %span.light Name: + %strong= @user.name + %li + %span.light Username: + %strong + = @user.username + %li + %span.light Email: + %strong + = mail_to @user.email + - @user.emails.each do |email| + %li + %span.light Secondary email: + %strong= email.email + = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do + %i.fa.fa-times - %li - %span.light Can create groups: - %strong - = @user.can_create_group ? "Yes" : "No" - %li - %span.light Personal projects limit: - %strong - = @user.projects_limit - %li - %span.light Member since: - %strong - = @user.created_at.stamp("Nov 12, 2031") - - if @user.confirmed_at - %li - %span.light Confirmed at: - %strong - = @user.confirmed_at.stamp("Nov 12, 2031") + %li.two-factor-status + %span.light Two-factor Authentication: + %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'} + - if @user.two_factor_enabled? + Enabled + = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication' - else - %li - %span.light Confirmed: - %strong.cred - No + Disabled - %li - %span.light Current sign-in at: - %strong - - if @user.current_sign_in_at - = @user.current_sign_in_at.stamp("Nov 12, 2031") - - else - never + %li + %span.light Can create groups: + %strong + = @user.can_create_group ? "Yes" : "No" + %li + %span.light Personal projects limit: + %strong + = @user.projects_limit + %li + %span.light Member since: + %strong + = @user.created_at.stamp("Nov 12, 2031") + - if @user.confirmed_at + %li + %span.light Confirmed at: + %strong + = @user.confirmed_at.stamp("Nov 12, 2031") + - else + %li + %span.light Confirmed: + %strong.cred + No - %li - %span.light Last sign-in at: - %strong - - if @user.last_sign_in_at - = @user.last_sign_in_at.stamp("Nov 12, 2031") - - else - never + %li + %span.light Current sign-in at: + %strong + - if @user.current_sign_in_at + = @user.current_sign_in_at.stamp("Nov 12, 2031") + - else + never - %li - %span.light Sign-in count: - %strong - = @user.sign_in_count + %li + %span.light Last sign-in at: + %strong + - if @user.last_sign_in_at + = @user.last_sign_in_at.stamp("Nov 12, 2031") + - else + never - - if @user.ldap_user? - %li - %span.light LDAP uid: - %strong - = @user.ldap_identity.extern_uid + %li + %span.light Sign-in count: + %strong + = @user.sign_in_count - - if @user.created_by - %li - %span.light Created by: - %strong - = link_to @user.created_by.name, [:admin, @user.created_by] + - if @user.ldap_user? + %li + %span.light LDAP uid: + %strong + = @user.ldap_identity.extern_uid - .col-md-6 - - unless @user == current_user - - if @user.blocked? - .panel.panel-info - .panel-heading - This user is blocked - .panel-body - %p Blocking user has the following effects: - %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li Personal projects will be left - %li Owned groups will be left - %br - = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - - else - .panel.panel-warning - .panel-heading - Block this user - .panel-body - %p Blocking user has the following effects: - %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li User will be removed from joined projects and groups - %li Personal projects will be left - %li Owned groups will be left - %br - = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" + - if @user.created_by + %li + %span.light Created by: + %strong + = link_to @user.created_by.name, [:admin, @user.created_by] - .panel.panel-danger - .panel-heading - Remove user - .panel-body - %p Deleting a user has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = @user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - - if @user.solo_owned_groups.present? - %li - Next groups with all content will be removed: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} - %br - = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" - - #profile.tab-pane - .row - .col-md-6 - .panel.panel-default + .col-md-6 + - unless @user == current_user + - unless @user.confirmed? + .panel.panel-info .panel-heading - = @user.name - %ul.well-list - %li - = image_tag avatar_icon(@user.email, 60), class: "avatar s60" - %li - %span.light Profile page: - %strong - = link_to user_path(@user) do - = @user.username - .col-md-6 - = render 'users/profile', user: @user + Confirm user + .panel-body + - if @user.unconfirmed_email.present? + - email = " (#{@user.unconfirmed_email})" + %p This user has an unconfirmed email address#{email}. You may force a confirmation. + %br + = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } + - if @user.blocked? + .panel.panel-info + .panel-heading + This user is blocked + .panel-body + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li Personal projects will be left + %li Owned groups will be left + %br + = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } + - else + .panel.panel-warning + .panel-heading + Block this user + .panel-body + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li User will be removed from joined projects and groups + %li Personal projects will be left + %li Owned groups will be left + %br + = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" + - if @user.access_locked? + .panel.panel-info + .panel-heading + This account has been locked + .panel-body + %p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account. + %br + = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - #groups.tab-pane - - if @user.group_members.present? - .panel.panel-default - .panel-heading Groups: - %ul.well-list - - @user.group_members.each do |group_member| - - group = group_member.group - %li.group_member - %span{class: ("list-item-name" unless group_member.owner?)} - %strong= link_to group.name, admin_group_path(group) - .pull-right - %span.light= group_member.human_access - - unless group_member.owner? - = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-times.fa-inverse - - else - .nothing-here-block This user has no groups. - - #projects.tab-pane - - if @user.groups.any? - .panel.panel-default - .panel-heading Group projects - %ul.well-list - - @user.groups.each do |group| - %li - %strong= group.name - – access to - #{pluralize(group.projects.count, 'project')} - - .row - .col-md-6 - = render 'users/projects', projects: @personal_projects - - .col-md-6 - .panel.panel-default - .panel-heading Joined projects (#{@joined_projects.count}) - %ul.well-list - - @joined_projects.sort_by(&:name_with_namespace).each do |project| - - member = project.team.find_member(@user.id) - %li.project_member - .list-item-name - = link_to admin_namespace_project_path(project.namespace, project), class: dom_class(project) do - = project.name_with_namespace - - - if member - .pull-right - - if member.owner? - %span.light Owner - - else - %span.light= member.human_access - - - if member.respond_to? :project - = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do - %i.fa.fa-times - #ssh-keys.tab-pane - = render 'profiles/keys/key_table', admin: true + .panel.panel-danger + .panel-heading + Remove user + .panel-body + - if @user.can_be_removed? + %p Deleting a user has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = @user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + %br + = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + - else + - if @user.solo_owned_groups.present? + %p + This user is currently an owner in these groups: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %p + You must transfer ownership or delete these groups before you can delete this user. diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index c1fc1602d0..213b5d65b3 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -1,4 +1,13 @@ -= render "events/event_last_push", event: @last_push -= render 'shared/event_filter' +.hidden-xs + = render "events/event_last_push", event: @last_push + + - if current_user + %ul.nav.nav-pills.event_filter.pull-right + %li.pull-right + = link_to dashboard_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do + %i.fa.fa-rss + + = render 'shared/event_filter' + %hr .content_list = spinner diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 0cb7f764fa..0a354373b9 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -1,7 +1,8 @@ +- page_title "Groups" %h3.page-title Group Membership - if current_user.can_create_group? - %span.pull-right + %span.pull-right.hidden-xs = link_to new_group_path, class: "btn btn-new" do %i.fa.fa-plus New Group @@ -16,18 +17,17 @@ - @group_members.each do |group_member| - group = group_member.group %li - .pull-right + .pull-right.hidden-xs - if can?(current_user, :admin_group, group) = link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do %i.fa.fa-cogs Settings - - if can?(current_user, :destroy_group_member, group_member) - = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Remove user from group' do - %i.fa.fa-sign-out - Leave + = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do + %i.fa.fa-sign-out + Leave - = image_tag group_icon(group), class: "avatar s40 avatar-tile" + = image_tag group_icon(group), class: "avatar s40 avatar-tile hidden-xs" = link_to group, class: 'group-name' do %strong= group.name diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 72e9e361dc..07bda1c77f 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{current_user.name} issues" - xml.link href: issues_dashboard_url(:atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml" - xml.link href: issues_dashboard_url(private_token: current_user.private_token), rel: "alternate", type: "text/html" - xml.id issues_dashboard_url(private_token: current_user.private_token) + xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" + xml.id issues_dashboard_url xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index db19a46cb2..94318d1bcf 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -1,3 +1,8 @@ +- page_title "Issues" += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues") + %h3.page-title Issues @@ -6,5 +11,11 @@ %hr .append-bottom-20 - = render 'shared/issuable_filter' + .pull-right + - if current_user + .hidden-xs.pull-left + = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do + %i.fa.fa-rss + + = render 'shared/issuable/filter', type: :issues = render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 97a42461b4..90611d562b 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -1,3 +1,4 @@ +- page_title "Merge Requests" %h3.page-title Merge Requests @@ -6,5 +7,5 @@ List all merge requests from all projects you have access to. %hr .append-bottom-20 - = render 'shared/issuable_filter' + = render 'shared/issuable/filter', type: :merge_requests = render 'shared/merge_requests' diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml index 21e730bb7f..d6f3e029a3 100644 --- a/app/views/dashboard/milestones/_milestone.html.haml +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -3,10 +3,10 @@ = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) .row .col-sm-6 - = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = link_to issues_dashboard_path(milestone_title: milestone.title) do = pluralize milestone.issue_count, 'Issue'   - = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do = pluralize milestone.merge_requests_count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 9944c0df81..9a9a5e139a 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Milestones" %h3.page-title Milestones %span.pull-right #{@dashboard_milestones.count} milestones diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 57cce9ab74..0d204ced7e 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,3 +1,4 @@ +- page_title @dashboard_milestone.title, "Milestones" %h4.page-title .issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" } - if @dashboard_milestone.closed? @@ -55,6 +56,9 @@ Participants %span.badge= @dashboard_milestone.participants.count + .pull-right + = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped" + .tab-content .tab-pane.active#tab-issues .row diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index f4ad2b294b..8aaa0a7f07 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -1,4 +1,7 @@ +- page_title "Starred Projects" - if @projects.any? + = render 'shared/show_aside' + .dashboard.row %section.activities.col-md-8 = render 'dashboard/activities' @@ -15,9 +18,6 @@ = render 'shared/projects_list', projects: @projects, projects_limit: 20, stars: true, avatar: false - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left - - else %h3 You don't have starred projects yet %p.slead Visit project page and press on star icon and it will appear on this page. diff --git a/app/views/dashboard/show.atom.builder b/app/views/dashboard/show.atom.builder index da631ecb33..e9a612231d 100644 --- a/app/views/dashboard/show.atom.builder +++ b/app/views/dashboard/show.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Dashboard feed#{" - #{current_user.name}" if current_user.name.present?}" - xml.link href: dashboard_url(:atom), rel: "self", type: "application/atom+xml" + xml.title "Activity" + xml.link href: dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_url, rel: "alternate", type: "text/html" - xml.id projects_url + xml.id dashboard_url xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index fa8946011b..5001c2101e 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -1,11 +1,15 @@ += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity") + - if @projects.any? + = render 'shared/show_aside' + .dashboard.row %section.activities.col-md-8 = render 'activities' %aside.col-md-4 = render 'sidebar' - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left - else = render "zero_authorized_projects" diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index e8820daf58..29ffe8a8be 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -6,7 +6,7 @@ .devise-errors = devise_error_messages! .clearfix.append-bottom-20 - = f.email_field :email, placeholder: "Email", class: "form-control", required: true + = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email] .clearfix = f.submit "Reset password", class: "btn-primary btn" diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index d3e37f7494..42cfbbf84f 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -1,3 +1,4 @@ +- page_title "Sign up" = render 'devise/shared/signup_box' -= render 'devise/shared/sign_in_link' \ No newline at end of file += render 'devise/shared/sign_in_link' diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 54a3972677..9f5520603c 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,5 +1,5 @@ = form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus" + = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off" = f.password_field :password, class: "form-control bottom", placeholder: "Password" - if devise_mapping.rememberable? .remember-me.checkbox diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 812e22373a..689cd6ed66 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,4 +1,9 @@ = form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} - = button_tag "#{server['label']} Sign in", class: "btn-save btn" + - if devise_mapping.rememberable? + .remember-me.checkbox + %label{for: "remember_me"} + = check_box_tag :remember_me, '1', false, id: 'remember_me' + %span Remember me + = button_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 89e4e229ac..dbc8eda619 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,3 +1,4 @@ +- page_title "Sign in" %div - if signin_enabled? || ldap_enabled? = render 'devise/shared/signin_box' diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml new file mode 100644 index 0000000000..22b2c1a186 --- /dev/null +++ b/app/views/devise/sessions/two_factor.html.haml @@ -0,0 +1,10 @@ +%div + .login-box + .login-heading + %h3 Two-factor Authentication + .login-body + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor authentication code', required: true, autofocus: true + %p.help-block.hint If you've lost your phone, you may enter one of your recovery codes. + .prepend-top-20 + = f.submit "Verify code", class: "btn btn-save" diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index b647b906b7..ecf680e7b2 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,10 +1,8 @@ %p %span.light Sign in with   - - providers = additional_providers + - providers = button_based_providers - providers.each do |provider| %span.light - - if default_providers.include?(provider) - = link_to oauth_image_tag(provider), omniauth_authorize_path(resource_name, provider), class: 'oauth-image-link' - - else - = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn" + - has_icon = provider_has_icon?(provider) + = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index c76574db45..bb5e479697 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -6,7 +6,7 @@ .login-heading %h3 Sign in .login-body - - if ldap_enabled? + - if form_based_providers.any? %ul.nav.nav-tabs - @ldap_servers.each_with_index do |server, i| %li{class: (:active if i.zero?)} diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index a5fec2fabd..98a61ab211 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -1,17 +1,22 @@ = form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f| - if application.errors.any? - .alert.alert-danger{"data-alert" => ""} - %p Whoops! Check your form for possible errors - = content_tag :div, class: "form-group#{' has-error' if application.errors[:name].present?}" do - = f.label :name, class: 'col-sm-2 control-label' + .alert.alert-danger + %ul + - application.errors.full_messages.each do |msg| + %li= msg + + .form-group + = f.label :name, class: 'control-label' + .col-sm-10 - = f.text_field :name, class: 'form-control' - = doorkeeper_errors_for application, :name - = content_tag :div, class: "form-group#{' has-error' if application.errors[:redirect_uri].present?}" do - = f.label :redirect_uri, class: 'col-sm-2 control-label' + = f.text_field :name, class: 'form-control', required: true + + .form-group + = f.label :redirect_uri, class: 'control-label' + .col-sm-10 - = f.text_area :redirect_uri, class: 'form-control' - = doorkeeper_errors_for application, :redirect_uri + = f.text_area :redirect_uri, class: 'form-control', required: true + %span.help-block Use one line per URI - if Doorkeeper.configuration.native_redirect_uri @@ -19,6 +24,7 @@ Use %code= Doorkeeper.configuration.native_redirect_uri for local tests + .form-actions - = f.submit 'Submit', class: "btn btn-primary wide" - = link_to "Cancel", applications_profile_path, class: "btn btn-default" + = f.submit 'Submit', class: "btn btn-create" + = link_to "Cancel", applications_profile_path, class: "btn btn-cancel" diff --git a/app/views/doorkeeper/applications/edit.html.haml b/app/views/doorkeeper/applications/edit.html.haml index 61584eb9c4..fb6aa30ace 100644 --- a/app/views/doorkeeper/applications/edit.html.haml +++ b/app/views/doorkeeper/applications/edit.html.haml @@ -1,2 +1,3 @@ +- page_title "Edit", @application.name, "Applications" %h3.page-title Edit application -= render 'form', application: @application \ No newline at end of file += render 'form', application: @application diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index e5be4b4bca..3b0b19107c 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Applications" %h3.page-title Your applications %p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' %table.table.table-striped @@ -13,4 +14,4 @@ %td= link_to application.name, oauth_application_path(application) %td= application.redirect_uri %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link' - %td= render 'delete_form', application: application \ No newline at end of file + %td= render 'delete_form', application: application diff --git a/app/views/doorkeeper/applications/new.html.haml b/app/views/doorkeeper/applications/new.html.haml index 655845e4af..fd32a468b4 100644 --- a/app/views/doorkeeper/applications/new.html.haml +++ b/app/views/doorkeeper/applications/new.html.haml @@ -1,2 +1,7 @@ -%h3.page-title New application +- page_title "New Application" + +%h3.page-title New Application + +%hr + = render 'form', application: @application \ No newline at end of file diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 82e78b4af1..80340aca54 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -1,3 +1,4 @@ +- page_title @application.name, "Applications" %h3.page-title Application: #{@application.name} diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index 4bba72167e..bfa95ce79a 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -1,4 +1,9 @@ - submit_btn_css ||= 'btn btn-link btn-remove' -= form_tag oauth_authorized_application_path(application) do +- if defined?(token) + - path = oauth_authorized_application_path(0, token_id: token) +- else + - path = oauth_authorized_application_path(application) + += form_tag path do %input{:name => "_method", :type => "hidden", :value => "delete"}/ - = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm' \ No newline at end of file + = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm' diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index a1d8664c4c..012e985764 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,3 +1,4 @@ +- page_title "Access Denied" %h1 403 %h3 Access Denied %hr diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml index 64c7451a8d..90cfbebfcc 100644 --- a/app/views/errors/encoding.html.haml +++ b/app/views/errors/encoding.html.haml @@ -1,3 +1,4 @@ +- page_title "Encoding Error" %h1 500 %h3 Encoding Error %hr diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml index 189e53bca5..ff5d4cc150 100644 --- a/app/views/errors/git_not_found.html.haml +++ b/app/views/errors/git_not_found.html.haml @@ -1,3 +1,4 @@ +- page_title "Git Resource Not Found" %h1 404 %h3 Git Resource Not found %hr diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index 7bf88f592c..3756b98ebb 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,3 +1,4 @@ +- page_title "Not Found" %h1 404 %h3 The resource you were looking for doesn't exist. %hr diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml index f3c8221a9d..3e70e98a24 100644 --- a/app/views/errors/omniauth_error.html.haml +++ b/app/views/errors/omniauth_error.html.haml @@ -1,3 +1,4 @@ +- page_title "Auth Error" %h1 422 %h3 Sign-in using #{@provider} auth failed %hr diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index c86ce9ae65..742b74a67c 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -2,4 +2,4 @@ .commit-row-title = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''   - = gfm event_commit_title(commit[:message]), project + = gfm event_commit_title(commit[:message]), project: project diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 02b1dec753..0faab4458e 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,14 +3,13 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = cache [event, current_user] do + = cache [event, "v1"] do = image_tag avatar_icon(event.author_email, 24), class: "avatar s24", alt:'' - - - if event.push? + - if event.created_project? + = render "events/event/created_project", event: event + - elsif event.push? = render "events/event/push", event: event - elsif event.commented? = render "events/event/note", event: event - - elsif event.created_project? - = render "events/event/created_project", event: event - else - = render "events/event/common", event: event \ No newline at end of file + = render "events/event/common", event: event diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index 0edb61ea24..4259f64c19 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,3 +1,3 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - if issue.description.present? - = markdown(issue.description, xhtml: true) + = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project) diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index d2f0005142..501412642d 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -4,7 +4,7 @@ %span You pushed to = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do %strong= event.ref_name - at + %span at %strong= link_to_project event.project #{time_ago_with_tooltip(event.created_at)} diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index 1a8b62abea..e8ed13df78 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,3 +1,3 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - if merge_request.description.present? - = markdown(merge_request.description, xhtml: true) + = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index b49c331ccf..cfbfba5020 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(note.note, xhtml: true) + = markdown(note.note, xhtml: true, reference_only_path: false, project: note.project) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 5d14def8f7..3625cb49d8 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -6,7 +6,7 @@ %i at = commit[:timestamp].to_time.to_s(:short) - %blockquote= markdown(escape_once(commit[:message]), xhtml: true) + %blockquote= markdown(escape_once(commit[:message]), xhtml: true, reference_only_path: false, project: event.project) - if event.commits_count > 15 %p %i diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 552525f4a0..8cf36c711b 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -8,8 +8,8 @@ - else = event.project_name -- if current_user == event.author && !event.project.private? && twitter_sharing_enabled? - .event-body +- if !event.project.private? && twitter_sharing_enabled? + .event-body{"data-user-is" => event.author_id} .event-note .md %p @@ -18,7 +18,7 @@ %a.twitter-share-button{ | href: "https://twitter.com/share", | "data-url" => event.project.web_url, | - "data-text" => "I just #{event.project.imported? ? "imported" : "created"} a new project in GitLab! GitLab is version control on your server.", | + "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", | "data-size" => "medium", | "data-related" => "gitlab", | "data-hashtags" => "gitlab", | diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 4ef18c0906..07bec1697f 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -14,7 +14,7 @@ .event-note .md %i.fa.fa-comment-o.event-note-icon - = event_note(event.target.note) + = event_note(event.target.note, project: event.project) - note = event.target - if note.attachment.url - if note.attachment.image? diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 60d7978b13..8bed5cdb9c 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -4,8 +4,8 @@ - if event.rm_ref? %strong= event.ref_name - else - = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do - %strong= event.ref_name + %strong + = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) at = link_to_project event.project @@ -17,15 +17,28 @@ - few_commits.each do |commit| = render "events/commit", commit: commit, project: project + - create_mr = event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project) - if event.commits_count > 1 %li.commits-stat - if event.commits_count > 2 %span ... and #{event.commits_count - 2} more commits. + - if event.md_ref? - from = event.commit_from - from_label = truncate_sha(from) - else - from = event.project.default_branch - from_label = from + = link_to namespace_project_compare_path(event.project.namespace, event.project, from: from, to: event.commit_to) do - %strong Compare → #{from_label}...#{truncate_sha(event.commit_to)} + Compare #{from_label}...#{truncate_sha(event.commit_to)} + + - if create_mr + %span{"data-user-is" => event.author_id, "data-display" => "inline"} + or + = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do + create a merge request + - elsif create_mr + %li.commits-stat{"data-user-is" => event.author_id} + = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do + Create Merge Request diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 2ea6cb1865..f3f0b77853 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -1,9 +1,10 @@ +- page_title "Groups" .clearfix .pull-left = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| = hidden_field_tag :sort, @sort .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "groups_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search" .form-group = button_tag 'Search', class: "btn btn-primary wide" diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index b3963a9d90..82622a58ed 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,7 +1,7 @@ .pull-left = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f| .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search" .form-group = button_tag 'Search', class: "btn btn-primary wide" diff --git a/app/views/explore/projects/_project.html.haml b/app/views/explore/projects/_project.html.haml index d65fb52937..1e8a89e366 100644 --- a/app/views/explore/projects/_project.html.haml +++ b/app/views/explore/projects/_project.html.haml @@ -9,12 +9,12 @@ .project-info - if project.description.present? - %p.project-description.str-truncated - = project.description + .project-description.str-truncated + = markdown(project.description, pipeline: :description) .repo-info - unless project.empty_repo? - = link_to pluralize(project.repository.round_commit_count, 'commit'), namespace_project_commits_path(project.namespace, project, project.default_branch) + = link_to pluralize(round_commit_count(project), 'commit'), namespace_project_commits_path(project.namespace, project, project.default_branch) · = link_to pluralize(project.repository.branch_names.count, 'branch'), namespace_project_branches_path(project.namespace, project) · diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 5086b58cd0..ba2276f51c 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Projects" .clearfix = render 'filter' diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index 420f069375..b5d146b1f2 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -1,3 +1,4 @@ +- page_title "Starred Projects" .explore-trending-block %p.lead %i.fa.fa-star diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 9cad923893..5e24df76a6 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -1,3 +1,10 @@ +- page_title "Trending Projects" +.explore-title + %h3 + Explore GitLab + %p.lead + Discover projects and groups. Share your projects with others +%hr .explore-trending-block %p.lead %i.fa.fa-comments-o @@ -6,6 +13,3 @@ .public-projects %ul.bordered-list = render @trending_projects - - .center - = link_to 'Show all projects', explore_projects_path, class: 'btn btn-primary' diff --git a/app/views/groups/_settings_nav.html.haml b/app/views/groups/_settings_nav.html.haml deleted file mode 100644 index e6aee22e52..0000000000 --- a/app/views/groups/_settings_nav.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -%ul.sidebar-subnav - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'Group' do - %i.fa.fa-pencil-square-o - %span - Group - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do - %i.fa.fa-folder - %span - Projects diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 49e7180bf9..2ff4b7e23e 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -10,8 +10,7 @@ = render 'shared/group_form', f: f .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' %p.light - if @group.avatar? diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 56b1948a47..b5f359279d 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -6,7 +6,8 @@ %span{class: ("list-item-name" if show_controls)} - if member.user = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' - %strong= user.name + %strong + = link_to user.name, user_path(user) %span.cgray= user.username - if user == current_user %span.label.label-success It's you @@ -24,7 +25,7 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if show_controls && can?(current_user, :admin_group, @group) + - if show_controls && can?(current_user, :admin_group_member, member) = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite @@ -32,7 +33,7 @@ %span.pull-right %strong= member.human_access - if show_controls - - if can?(current_user, :modify_group_member, member) + - if can?(current_user, :update_group_member, member) = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o @@ -40,7 +41,8 @@   - if current_user == user = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + = icon("sign-out") + Leave - else = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index c0c9cd170a..dba395cc8f 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Members" - show_roles = should_user_see_group_roles?(current_user, @group) %h3.page-title @@ -13,10 +14,10 @@ .clearfix.js-toggle-container = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' } + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } = button_tag 'Search', class: 'btn' - - if current_user && current_user.can?(:admin_group, @group) + - if current_user && current_user.can?(:admin_group_member, @group) .pull-right = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do Add members diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 240001967f..66fe7e2587 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@user.name} issues" - xml.link :href => issues_dashboard_url(:atom, :private_token => @user.private_token), :rel => "self", :type => "application/atom+xml" - xml.link :href => issues_dashboard_url(:private_token => @user.private_token), :rel => "alternate", :type => "text/html" - xml.id issues_dashboard_url(:private_token => @user.private_token) + xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml" + xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" + xml.id issues_dashboard_url xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 6c0d89c4e7..f0d9078255 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,3 +1,8 @@ +- page_title "Issues" += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues") + %h3.page-title Issues @@ -10,5 +15,11 @@ %hr .append-bottom-20 - = render 'shared/issuable_filter' + .pull-right + - if current_user + .hidden-xs.pull-left + = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do + %i.fa.fa-rss + + = render 'shared/issuable/filter', type: :issues = render 'shared/issues' diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 1ad7490563..ca85a15870 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -1,3 +1,4 @@ +- page_title "Merge Requests" %h3.page-title Merge Requests @@ -9,5 +10,5 @@ To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. %hr .append-bottom-20 - = render 'shared/issuable_filter' + = render 'shared/issuable/filter', type: :merge_requests = render 'shared/merge_requests' diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index 30093d2d05..ba30e6e07c 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -9,10 +9,10 @@ = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) .row .col-sm-6 - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do + = link_to issues_group_path(@group, milestone_title: milestone.title) do = pluralize milestone.issue_count, 'Issue'   - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do + = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do = pluralize milestone.merge_requests_count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 008d5a6bd2..385222fa5b 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Milestones" %h3.page-title Milestones %span.pull-right #{@group_milestones.count} milestones diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index fb32f2caa4..8f2decb851 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,3 +1,4 @@ +- page_title @group_milestone.title, "Milestones" %h4.page-title .issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" } - if @group_milestone.closed? @@ -61,6 +62,9 @@ Participants %span.badge= @group_milestone.participants.count + .pull-right + = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped" + .tab-content .tab-pane.active#tab-issues .row diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 6e17cdaef6..0665cdf387 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -1,3 +1,5 @@ +- page_title 'New Group' +- header_title 'New Group' = form_for @group, html: { class: 'group-form form-horizontal' } do |f| - if @group.errors.any? .alert.alert-danger @@ -11,8 +13,7 @@ = render 'shared/choose_group_avatar_button', f: f .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 = render 'shared/group_tips' .form-actions diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 0d547984cc..6b7efa83de 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,3 +1,4 @@ +- page_title "Projects" .panel.panel-default .panel-heading %strong= @group.name diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index c78bd1bd26..a91d1a6e94 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Group feed - #{@group.name}" - xml.link href: group_path(@group, :atom), rel: "self", type: "application/atom+xml" - xml.link href: group_path(@group), rel: "alternate", type: "text/html" - xml.id projects_url + xml.title "#{@group.name} activity" + xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: group_url(@group), rel: "alternate", type: "text/html" + xml.id group_url(@group) xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 8df9366ecb..d31dae7d64 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,3 +1,7 @@ += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") + .dashboard .header-with-avatar.clearfix = image_tag group_icon(@group), class: "avatar group-avatar s90" @@ -7,16 +11,27 @@ @#{@group.path} - if @group.description.present? .description - = escaped_autolink(@group.description) + = markdown(@group.description, pipeline: :description) %hr + + = render 'shared/show_aside' + .row %section.activities.col-md-8 - - if current_user - = render "events/event_last_push", event: @last_push - = render 'shared/event_filter' + .hidden-xs + - if current_user + = render "events/event_last_push", event: @last_push + + - if current_user + %ul.nav.nav-pills.event_filter.pull-right + %li + = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do + %i.fa.fa-rss + + = render 'shared/event_filter' + %hr + .content_list = spinner %aside.side.col-md-4 = render "projects", projects: @projects - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 7b21ca30d8..e809d99ba7 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -1,4 +1,4 @@ -#modal-shortcuts.modal.hide{tabindex: -1} +#modal-shortcuts.modal{tabindex: -1} .modal-dialog .modal-content .modal-header @@ -79,6 +79,12 @@ %td.shortcut .key g .key p + %td + Go to the project's home page + %tr + %td.shortcut + .key g + .key e %td Go to the project's activity feed %tr @@ -187,7 +193,11 @@ %td.shortcut .key m %td Change milestone - %tbody{ class: 'hidden-shortcut merge_reuests', style: 'display:none' } + %tr + %td.shortcut + .key r + %td Reply (quoting selected text) + %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' } %tr %th %th Merge Requests @@ -199,6 +209,10 @@ %td.shortcut .key m %td Change milestone + %tr + %td.shortcut + .key r + %td Reply (quoting selected text) :javascript diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index af39dfeac5..bf4b7234b2 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -3,6 +3,8 @@ GitLab %span= Gitlab::VERSION %small= Gitlab::REVISION + - if current_application_settings.version_check_enabled + = version_status_badge %p.slead GitLab is open source software to collaborate on code. %br diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index cc1be6a717..8551496b98 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,2 +1,3 @@ +- page_title @file.humanize, *@category.split("/").reverse.map(&:humanize) .documentation.wiki = markdown @markdown.gsub('$your_email', current_user.email) diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 246a6c1bdf..7c89457ace 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -1,3 +1,4 @@ +- page_title "UI Development Kit", "Help" - lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare." .gitlab-ui-dev-kit diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index 90a6f5f9d2..d8af0295b2 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -14,12 +14,16 @@ :plain job = $("tr#repo_#{@repo_id}") job.find(".import-actions").html("

    Access denied! Please verify you can add deploy keys to this repository.

    ") -- else +- elsif @project.persisted? :plain job = $("tr#repo_#{@repo_id}") job.attr("id", "project_#{@project.id}") target_field = job.find(".import-target") target_field.empty() - target_field.append('#{link_to @project.path_with_namespace, [@project.namespace.becomes(Namespace), @project]}') + target_field.append('#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}') $("table.import-jobs tbody").prepend(job) job.addClass("active").find(".import-actions").html(" started") +- else + :plain + job = $("tr#repo_#{@repo_id}") + job.find(".import-actions").html(" Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 4e49bbbc7f..777eb48271 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -1,12 +1,18 @@ +- page_title "Bitbucket import" %h3.page-title %i.fa.fa-bitbucket Import projects from Bitbucket -%p.light - Select projects you want to import. -%hr -%p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" +- if @repos.any? + %p.light + Select projects you want to import. + %hr + %p + - if @incompatible_repos.any? + = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all" + - else + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + %table.table.import-jobs %thead @@ -40,6 +46,24 @@ = "#{repo["owner"]}/#{repo["slug"]}" %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" + - @incompatible_repos.each do |repo| + %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %td + = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + %td.import-target + %td.import-actions-job-status + = label_tag "Incompatible Project", nil, class: "label label-danger" + +- if @incompatible_repos.any? + %p + One or more of your Bitbucket projects cannot be imported into GitLab + directly because they use Subversion or Mercurial for version control, + rather than Git. Please convert + = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview" + and go through the + = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true" + again. + :coffeescript new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}") diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index f0bc3e6b1a..ef55249823 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,3 +1,4 @@ +- page_title "GitHub import" %h3.page-title %i.fa.fa-github Import projects from GitHub diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 33b0a21acf..727f3c7e7f 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -1,3 +1,4 @@ +- page_title "GitLab.com import" %h3.page-title %i.fa.fa-heart Import projects from GitLab.com diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 78c5e957be..bff7ee7c85 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -1,3 +1,4 @@ +- page_title "Gitorious import" %h3.page-title %i.icon-gitorious.icon-gitorious-big Import projects from Gitorious.org diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index ce78fec205..9c64e0a009 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -1,3 +1,4 @@ +- page_title "Google Code import" %h3.page-title %i.fa.fa-google Import projects from Google Code diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml index 9c6824ecad..e53ebda7dc 100644 --- a/app/views/import/google_code/new_user_map.html.haml +++ b/app/views/import/google_code/new_user_map.html.haml @@ -1,3 +1,4 @@ +- page_title "User map", "Google Code import" %h3.page-title %i.fa.fa-google Import projects from Google Code diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 2013b8c03c..e8ec79e72f 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -1,16 +1,21 @@ +- page_title "Google Code import" %h3.page-title %i.fa.fa-google Import projects from Google Code -%p.light - Select projects you want to import. -%p.light - Optionally, you can - = link_to "customize", new_user_map_import_google_code_path - how Google Code email addresses and usernames are imported into GitLab. -%hr -%p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" +- if @repos.any? + %p.light + Select projects you want to import. + %p.light + Optionally, you can + = link_to "customize", new_user_map_import_google_code_path + how Google Code email addresses and usernames are imported into GitLab. + %hr + %p + - if @incompatible_repos.any? + = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all" + - else + = button_tag 'Import all projects', class: "btn btn-success js-import-all" %table.table.import-jobs %thead @@ -44,6 +49,22 @@ = "#{current_user.username}/#{repo.name}" %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" + - @incompatible_repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + %td.import-target + %td.import-actions-job-status + = label_tag "Incompatible Project", nil, class: "label label-danger" + +- if @incompatible_repos.any? + %p + One or more of your Google Code projects cannot be imported into GitLab + directly because they use Subversion or Mercurial for version control, + rather than Git. Please convert them to Git on Google Code, and go + through the + = link_to "import flow", new_import_google_code_path + again. :coffeescript new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}") diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml index ab0ecffe4d..2fd4859c1c 100644 --- a/app/views/invites/show.html.haml +++ b/app/views/invites/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Invitation" %h3.page-title Invitation %p diff --git a/app/views/layouts/_bootlint.haml b/app/views/layouts/_bootlint.haml new file mode 100644 index 0000000000..69280687a9 --- /dev/null +++ b/app/views/layouts/_bootlint.haml @@ -0,0 +1,4 @@ +:javascript + jQuery(document).ready(function() { + javascript:(function(){var s=document.createElement("script");s.onload=function(){bootlint.showLintReportForCurrentDocument([], {hasProblems: false, problemFree: false});};s.src="https://maxcdn.bootstrapcdn.com/bootlint/latest/bootlint.min.js";document.body.appendChild(s)})(); + }); diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index d12145651a..397649dacf 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,28 +1,39 @@ +- page_title "GitLab" %head %meta{charset: "utf-8"} + %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} %meta{content: "GitLab Community Edition", name: "description"} - %title - = "#{title} | " if defined?(title) - GitLab + %title= page_title + = favicon_link_tag 'favicon.ico' - = stylesheet_link_tag "application", :media => "all" - = stylesheet_link_tag "print", :media => "print" + + = stylesheet_link_tag "application", media: "all" + = stylesheet_link_tag "print", media: "print" + = javascript_include_tag "application" + = csrf_meta_tags + = include_gon + %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} %meta{name: 'theme-color', content: '#474D57'} + -# Apple Safari/iOS home screen icons + = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon' + = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76' + = favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120' + = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152' + + -# Windows 8 pinned site tile + %meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')} + %meta{name: 'msapplication-TileColor', content: '#30353E'} + + = yield :meta_tags + = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') + = render 'layouts/bootlint' if Rails.env.development? - -# Atom feed - - if current_user - - if controller_name == 'projects' && action_name == 'index' - = auto_discovery_link_tag :atom, projects_url(:atom, private_token: current_user.private_token), title: "Dashboard feed" - - if @project && !@project.new_record? - - if current_controller?(:tree, :commits) - = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "Recent commits to #{@project.name}:#{@ref}") - - if current_controller?(:issues) - = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") + = render 'layouts/user_styles' diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml deleted file mode 100644 index d58582c107..0000000000 --- a/app/views/layouts/_head_panel.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab - .navbar-inner - .container - %div.app_logo - = link_to root_path, class: "home has_bottom_tooltip", title: "Dashboard" do - = brand_header_logo - %h1.title= title - - %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} - %span.sr-only Toggle navigation - %i.fa.fa-bars - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.hidden-sm.hidden-xs - = render "layouts/search" - %li.visible-sm.visible-xs - = link_to search_path, title: "Search", class: 'has_bottom_tooltip', 'data-original-title' => 'Search area' do - %i.fa.fa-search - %li - = link_to help_path, title: 'Help', class: 'has_bottom_tooltip', - 'data-original-title' => 'Help' do - %i.fa.fa-question-circle - %li - = link_to explore_root_path, title: "Explore", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do - %i.fa.fa-globe - %li - = link_to user_snippets_path(current_user), title: "Your snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'Your snippets' do - %i.fa.fa-clipboard - - if current_user.is_admin? - %li - = link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do - %i.fa.fa-cogs - - if current_user.can_create_project? - %li - = link_to new_project_path, title: "New project", class: 'has_bottom_tooltip', 'data-original-title' => 'New project' do - %i.fa.fa-plus - %li - = link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do - %i.fa.fa-user - %li - = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Sign out", class: 'has_bottom_tooltip', 'data-original-title' => 'Sign out' do - %i.fa.fa-sign-out - %li.hidden-xs - = link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do - = image_tag avatar_icon(current_user.email, 60), alt: 'User activity' - -= render 'shared/outdated_browser' diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 422966cdc5..96e15783a3 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,23 +1,20 @@ -- if defined?(sidebar) - .page-with-sidebar{ class: nav_sidebar_class } - = render "layouts/broadcast" - .sidebar-wrapper - = render(sidebar) - .collapse-nav - = render partial: 'layouts/collapse_button' - .content-wrapper - .container-fluid - .content - = render "layouts/flash" - .clearfix - = yield -- else - .container.navless-container - .content - = yield - -= yield :embedded_scripts - -:coffeescript - $('.page-sidebar-collapsed .nav-sidebar a').tooltip placement: "right" - +.page-with-sidebar{ class: nav_sidebar_class } + = render "layouts/broadcast" + .sidebar-wrapper.nicescroll + - if defined?(sidebar) && sidebar + = render "layouts/nav/#{sidebar}" + - elsif current_user + = render 'layouts/nav/dashboard' + .collapse-nav + = render partial: 'layouts/collapse_button' + - if current_user + = link_to current_user, class: 'sidebar-user' do + = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s32' + .username + = current_user.username + .content-wrapper + .container-fluid + .content + = render "layouts/flash" + .clearfix + = yield diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml deleted file mode 100644 index 3d6d2bfc00..0000000000 --- a/app/views/layouts/_public_head_panel.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab - .navbar-inner - .container - %div.app_logo - = link_to explore_root_path, class: "home" do - = brand_header_logo - %h1.title= title - - %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} - %span.sr-only Toggle navigation - %i.fa.fa-bars - - - unless current_controller?('sessions') - .pull-right.hidden-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new append-right-10' - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.visible-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes') - -= render 'shared/outdated_browser' diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 04f7984685..e2d2dec7ab 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,6 +1,6 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input" + = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control" = hidden_field_tag :group_id, @group.try(:id) - if @project && @project.persisted? = hidden_field_tag :project_id, @project.id diff --git a/app/views/layouts/_user_styles.html.haml b/app/views/layouts/_user_styles.html.haml new file mode 100644 index 0000000000..b76b3cb551 --- /dev/null +++ b/app/views/layouts/_user_styles.html.haml @@ -0,0 +1,24 @@ +:css + [data-user-is] { + display: none !important; + } + + [data-user-is="#{current_user.try(:id)}"] { + display: block !important; + } + + [data-user-is="#{current_user.try(:id)}"][data-display="inline"] { + display: inline !important; + } + + [data-user-is-not] { + display: block !important; + } + + [data-user-is-not][data-display="inline"] { + display: inline !important; + } + + [data-user-is-not="#{current_user.try(:id)}"] { + display: none !important; + } diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index ab84e87c30..1c738719bd 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,6 +1,5 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: "Admin area" - %body{class: "#{app_theme} admin", :'data-page' => body_data_page} - = render "layouts/head_panel", title: link_to("Admin area", admin_root_path) - = render 'layouts/page', sidebar: 'layouts/nav/admin' +- page_title "Admin area" +- header_title "Admin area", admin_root_path +- sidebar "admin" + += render template: "layouts/application" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 6bd8ac4adb..678ed3c2c1 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,6 +1,15 @@ !!! 5 %html{ lang: "en"} - = render "layouts/head", title: "Dashboard" - %body{class: "#{app_theme} application", :'data-page' => body_data_page } - = render "layouts/head_panel", title: link_to("Dashboard", root_path) - = render 'layouts/page', sidebar: 'layouts/nav/dashboard' + = render "layouts/head" + %body{class: "#{user_application_theme}", 'data-page' => body_data_page} + -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. + = yield :scripts_body_top + + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/page', sidebar: sidebar + + = yield :scripts_body diff --git a/app/views/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml new file mode 100644 index 0000000000..c72eca10bf --- /dev/null +++ b/app/views/layouts/dashboard.html.haml @@ -0,0 +1,5 @@ +- page_title "Dashboard" +- header_title "Dashboard", root_path +- sidebar "dashboard" + += render template: "layouts/application" diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 6f805f1c9d..1987bf1592 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,9 +1,9 @@ !!! 5 %html{ lang: "en"} = render "layouts/head" - %body.ui_mars.login-page.application + %body.ui_charcoal.login-page.application + = render "layouts/header/empty" = render "layouts/broadcast" - = render "layouts/public_head_panel", title: '' .container.navless-container .content = render "layouts/flash" diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index e51fd4cb82..2af265a229 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,8 +1,8 @@ !!! 5 %html{ lang: "en"} - = render "layouts/head", title: "Error" - %body{class: "#{app_theme} application"} - = render "layouts/head_panel", title: "" if current_user + = render "layouts/head" + %body{class: "#{user_application_theme} application"} + = render "layouts/header/empty" .container.navless-container = render "layouts/flash" .error-page diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index 2bd0b8d85c..56bb92a536 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -1,30 +1,5 @@ -- page_title = 'Explore' -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: page_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - - if current_user - = render "layouts/head_panel", title: link_to(page_title, explore_root_path) - - else - = render "layouts/public_head_panel", title: link_to(page_title, explore_root_path) - .container.navless-container - .content - .explore-title - %h3 - Explore GitLab - %p.lead - Discover projects and groups. Share your projects with others +- page_title "Explore" +- header_title "Explore GitLab", explore_root_path +- sidebar "explore" - - %ul.nav.nav-tabs - = nav_link(path: 'projects#trending') do - = link_to 'Trending Projects', explore_root_path - = nav_link(path: 'projects#starred') do - = link_to 'Most Starred Projects', starred_explore_projects_path - = nav_link(path: 'projects#index') do - = link_to 'All Projects', explore_projects_path - = nav_link(controller: :groups) do - = link_to 'All Groups', explore_groups_path - - = yield += render template: "layouts/application" diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index f4a6bee15f..db7dbf9bfe 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,6 +1,5 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: group_head_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/head_panel", title: link_to(@group.name, group_path(@group)) - = render 'layouts/page', sidebar: 'layouts/nav/group' +- page_title @group.name +- header_title @group.name, group_path(@group) +- sidebar "group" unless sidebar + += render template: "layouts/application" diff --git a/app/views/layouts/group_settings.html.haml b/app/views/layouts/group_settings.html.haml new file mode 100644 index 0000000000..e303a56162 --- /dev/null +++ b/app/views/layouts/group_settings.html.haml @@ -0,0 +1,4 @@ +- page_title "Settings" +- sidebar "group_settings" + += render template: "layouts/group" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml new file mode 100644 index 0000000000..b3cd7b0e37 --- /dev/null +++ b/app/views/layouts/header/_default.html.haml @@ -0,0 +1,46 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } + .container + .header-logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = brand_header_logo + .gitlab-text-container + %h3 GitLab + .header-content + %button.navbar-toggle{type: 'button'} + %span.sr-only Toggle navigation + = icon('bars') + + .navbar-collapse.collapse + %ul.nav.navbar-nav.pull-right + %li.hidden-sm.hidden-xs + = render 'layouts/search' + %li.visible-sm.visible-xs + = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('search') + %li.hidden-xs + = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('question-circle fw') + %li + = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('globe fw') + %li + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('clipboard fw') + - if current_user.is_admin? + %li + = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('wrench fw') + - if current_user.can_create_project? + %li.hidden-xs + = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('plus fw') + %li + = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('cog fw') + %li + = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('sign-out') + + %h1.title= title + += render 'shared/outdated_browser' diff --git a/app/views/layouts/header/_empty.html.haml b/app/views/layouts/header/_empty.html.haml new file mode 100644 index 0000000000..2ed4edb113 --- /dev/null +++ b/app/views/layouts/header/_empty.html.haml @@ -0,0 +1,4 @@ +%header.navbar.navbar-fixed-top.navbar-empty + .container + .center-logo + = brand_header_logo diff --git a/app/views/layouts/header/_public.html.haml b/app/views/layouts/header/_public.html.haml new file mode 100644 index 0000000000..15c2e292be --- /dev/null +++ b/app/views/layouts/header/_public.html.haml @@ -0,0 +1,15 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } + .container + .header-logo + = link_to explore_root_path, class: "home" do + = brand_header_logo + .gitlab-text-container + %h3 GitLab + .header-content + - unless current_controller?('sessions') + .pull-right + = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success btn-sm' + + %h1.title= title + += render 'shared/outdated_browser' diff --git a/app/views/layouts/help.html.haml b/app/views/layouts/help.html.haml new file mode 100644 index 0000000000..224b24befb --- /dev/null +++ b/app/views/layouts/help.html.haml @@ -0,0 +1,4 @@ +- page_title "Help" +- header_title "Help", help_path + += render template: "layouts/application" diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 34efceb37d..2065be3828 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -1,65 +1,71 @@ %ul.nav.nav-sidebar = nav_link(controller: :dashboard, html_options: {class: 'home'}) do = link_to admin_root_path, title: "Stats" do - %i.fa.fa-dashboard + = icon('dashboard fw') %span Overview = nav_link(controller: :projects) do - = link_to admin_namespaces_projects_path, title: 'Projects' do - %i.fa.fa-cube + = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do + = icon('cube fw') %span Projects = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - %i.fa.fa-user + = link_to admin_users_path, title: 'Users', data: {placement: 'right'} do + = icon('user fw') %span Users = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - %i.fa.fa-group + = link_to admin_groups_path, title: 'Groups', data: {placement: 'right'} do + = icon('group fw') %span Groups = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - %i.fa.fa-key + = link_to admin_deploy_keys_path, title: 'Deploy Keys', data: {placement: 'right'} do + = icon('key fw') %span Deploy Keys = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - %i.fa.fa-file-text + = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do + = icon('file-text fw') %span Logs = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Broadcast Messages' do - %i.fa.fa-bullhorn + = link_to admin_broadcast_messages_path, title: 'Broadcast Messages', data: {placement: 'right'} do + = icon('bullhorn fw') %span Messages = nav_link(controller: :hooks) do - = link_to admin_hooks_path, title: 'Hooks' do - %i.fa.fa-external-link + = link_to admin_hooks_path, title: 'Hooks', data: {placement: 'right'} do + = icon('external-link fw') %span Hooks = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - %i.fa.fa-cog + = link_to admin_background_jobs_path, title: 'Background Jobs', data: {placement: 'right'} do + = icon('cog fw') %span Background Jobs = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do - %i.fa.fa-cloud + = link_to admin_applications_path, title: 'Applications', data: {placement: 'right'} do + = icon('cloud fw') %span Applications = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do - %i.fa.fa-copy + = link_to admin_application_settings_services_path, title: 'Service Templates', data: {placement: 'right'} do + = icon('copy fw') %span Service Templates + = nav_link(controller: :abuse_reports) do + = link_to admin_abuse_reports_path, title: "Abuse reports" do + = icon('exclamation-circle fw') + %span + Abuse Reports + %span.count= AbuseReport.count(:all) + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do - = link_to admin_application_settings_path, title: 'Settings' do - %i.fa.fa-cogs + = link_to admin_application_settings_path, title: 'Settings', data: {placement: 'right'} do + = icon('cogs fw') %span Settings - diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index e4f630c6a1..687c1fc3dd 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,38 +1,38 @@ %ul.nav.nav-sidebar - = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do - = link_to root_path, title: 'Home', class: 'shortcuts-activity' do - %i.fa.fa-dashboard + = nav_link(path: ['dashboard#show', 'root#show'], html_options: {class: 'home'}) do + = link_to dashboard_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = icon('dashboard fw') %span Your Projects = nav_link(path: 'projects#starred') do - = link_to starred_dashboard_projects_path, title: 'Starred Projects' do - %i.fa.fa-star + = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do + = icon('star fw') %span Starred Projects = nav_link(controller: :groups) do - = link_to dashboard_groups_path, title: 'Groups' do - %i.fa.fa-group + = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do + = icon('group fw') %span Groups = nav_link(controller: :milestones) do - = link_to dashboard_milestones_path, title: 'Milestones' do - %i.fa.fa-clock-o + = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do + = icon('clock-o fw') %span Milestones = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do - %i.fa.fa-exclamation-circle + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') %span Issues %span.count= current_user.assigned_issues.opened.count = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do - %i.fa.fa-tasks + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = icon('tasks fw') %span Merge Requests %span.count= current_user.assigned_merge_requests.opened.count = nav_link(controller: :help) do - = link_to help_path, title: 'Help' do - %i.fa.fa-question-circle + = link_to help_path, title: 'Help', data: {placement: 'right'} do + = icon('question-circle fw') %span Help diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml new file mode 100644 index 0000000000..66870e84ce --- /dev/null +++ b/app/views/layouts/nav/_explore.html.haml @@ -0,0 +1,18 @@ +%ul.nav.nav-sidebar + = nav_link(path: 'projects#trending') do + = link_to explore_root_path, title: 'Trending Projects', data: {placement: 'right'} do + = icon('comments fw') + %span Trending Projects + = nav_link(path: 'projects#starred') do + = link_to starred_explore_projects_path, title: 'Most-starred Projects', data: {placement: 'right'} do + = icon('star fw') + %span Most-starred Projects + = nav_link(path: 'projects#index') do + = link_to explore_projects_path, title: 'All Projects', data: {placement: 'right'} do + = icon('bookmark fw') + %span All Projects + = nav_link(controller: :groups) do + = link_to explore_groups_path, title: 'All Groups', data: {placement: 'right'} do + = icon('group fw') + %span All Groups + diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f0d92b7a12..695ce68a20 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,42 +1,45 @@ %ul.nav.nav-sidebar - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: "Home" do - %i.fa.fa-dashboard + = nav_link do + = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') %span - Activity + Back to Dashboard + + %li.separate-item + + = nav_link(path: 'groups#show', html_options: {class: 'home'}) do + = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do + = icon('dashboard fw') + %span + Group - if current_user = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones' do - %i.fa.fa-clock-o + = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do + = icon('clock-o fw') %span Milestones = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group), title: 'Issues' do - %i.fa.fa-exclamation-circle + = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') %span Issues - if current_user %span.count= Issue.opened.of_group(@group).count = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - %i.fa.fa-tasks + = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do + = icon('tasks fw') %span Merge Requests - if current_user %span.count= MergeRequest.opened.of_group(@group).count = nav_link(controller: [:group_members]) do - = link_to group_group_members_path(@group), title: 'Members' do - %i.fa.fa-users + = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do + = icon('users fw') %span Members - - if can?(current_user, :admin_group, @group) - = nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do - = link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do - %i.fa.fa-cogs + = nav_link(html_options: { class: "separate-item" }) do + = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do + = icon ('cogs fw') %span Settings - %i.fa.fa-angle-down - - - if group_settings_page? - = render 'groups/settings_nav' diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml new file mode 100644 index 0000000000..8075fe32fb --- /dev/null +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -0,0 +1,20 @@ +%ul.nav.nav-sidebar + = nav_link do + = link_to group_path(@group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to group + + %li.separate-item + + %ul.sidebar-subnav + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'Group Settings', data: {placement: 'right'} do + = icon ('pencil-square-o fw') + %span + Group Settings + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do + = icon('folder fw') + %span + Projects diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index d88e862829..33fd5fcef6 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,50 +1,59 @@ %ul.nav.nav-sidebar + = nav_link do + = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to Dashboard + + %li.separate-item + = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: "Profile" do - %i.fa.fa-user + = link_to profile_path, title: 'Profile', data: {placement: 'right'} do + = icon('user fw') %span Profile - = nav_link(controller: :accounts) do - = link_to profile_account_path, title: 'Account' do - %i.fa.fa-gear + = nav_link(controller: [:accounts, :two_factor_auths]) do + = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do + = icon('gear fw') %span Account - = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new']) do - = link_to applications_profile_path, title: 'Applications' do - %i.fa.fa-cloud + = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do + = link_to applications_profile_path, title: 'Applications', data: {placement: 'right'} do + = icon('cloud fw') %span Applications = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do - %i.fa.fa-envelope-o + = link_to profile_emails_path, title: 'Emails', data: {placement: 'right'} do + = icon('envelope-o fw') %span Emails %span.count= current_user.emails.count + 1 - unless current_user.ldap_user? = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do - %i.fa.fa-lock + = link_to edit_profile_password_path, title: 'Password', data: {placement: 'right'} do + = icon('lock fw') %span Password = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do - %i.fa.fa-inbox + = link_to profile_notifications_path, title: 'Notifications', data: {placement: 'right'} do + = icon('inbox fw') %span Notifications = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do - %i.fa.fa-key + = link_to profile_keys_path, title: 'SSH Keys', data: {placement: 'right'} do + = icon('key fw') %span SSH Keys %span.count= current_user.keys.count - = nav_link(path: 'profiles#design') do - = link_to design_profile_path, title: 'Design' do - %i.fa.fa-image + = nav_link(controller: :preferences) do + = link_to profile_preferences_path, title: 'Preferences', data: {placement: 'right'} do + -# TODO (rspeicher): Better icon? + = icon('image fw') %span - Design - = nav_link(path: 'profiles#history') do - = link_to history_profile_path, title: 'History' do - %i.fa.fa-history + Preferences + = nav_link(path: 'profiles#audit_log') do + = link_to audit_log_profile_path, title: 'Audit Log', data: {placement: 'right'} do + = icon('history fw') %span - History + Audit Log diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 6c13f30f62..d17d1c5fbd 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,97 +1,112 @@ -%ul.project-navigation.nav.nav-sidebar - - if @project_settings_nav +%ul.nav.nav-sidebar + - if @project.group = nav_link do - = link_to project_path(@project), title: 'Back to project', class: "" do - %i.fa.fa-caret-square-o-left + = link_to group_path(@project.group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') %span - Back to project - - %li.separate-item - - = render 'projects/settings_nav' - + Back to Group - else - = nav_link(path: 'projects#show', html_options: {class: "home"}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - %i.fa.fa-dashboard + = nav_link do + = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') %span - Project - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do - = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree' do - %i.fa.fa-files-o - %span - Files + Back to Dashboard - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches)) do - = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits' do - %i.fa.fa-history - %span - Commits + %li.separate-item - - if project_nav_tab? :network - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network' do - %i.fa.fa-code-fork - %span - Network + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do + = icon('home fw') + %span + Project + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: 'Project Activity', class: 'shortcuts-project-activity', data: {placement: 'right'} do + = icon('dashboard fw') + %span + Activity + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do + = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do + = icon('files-o fw') + %span + Files - - if project_nav_tab? :graphs - = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs' do - %i.fa.fa-area-chart - %span - Graphs + - if project_nav_tab? :commits + = nav_link(controller: %w(commit commits compare repositories tags branches)) do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do + = icon('history fw') + %span + Commits - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - %i.fa.fa-clock-o - %span - Milestones + - if project_nav_tab? :network + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do + = icon('code-fork fw') + %span + Network - - if project_nav_tab? :issues - = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do - %i.fa.fa-exclamation-circle - %span - Issues - - if @project.default_issues_tracker? - %span.count.issue_counter= @project.issues.opened.count + - if project_nav_tab? :graphs + = nav_link(controller: %w(graphs)) do + = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do + = icon('area-chart fw') + %span + Graphs - - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - %i.fa.fa-tasks - %span - Merge Requests - %span.count.merge_counter= @project.merge_requests.opened.count + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones', data: {placement: 'right'} do + = icon('clock-o fw') + %span + Milestones - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - %i.fa.fa-tags - %span - Labels + - if project_nav_tab? :issues + = nav_link(controller: :issues) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') + %span + Issues + - if @project.default_issues_tracker? + %span.count.issue_counter= @project.issues.opened.count - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - %i.fa.fa-book - %span - Wiki + - if project_nav_tab? :merge_requests + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = icon('tasks fw') + %span + Merge Requests + %span.count.merge_counter= @project.merge_requests.opened.count - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do - %i.fa.fa-file-text-o - %span - Snippets + - if project_nav_tab? :settings + = nav_link(controller: [:project_members, :teams]) do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do + = icon('users fw') + %span + Members - - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do - %i.fa.fa-cogs - %span - Settings + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels', data: {placement: 'right'} do + = icon('tags fw') + %span + Labels + + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki', data: {placement: 'right'} do + = icon('book fw') + %span + Wiki + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets', data: {placement: 'right'} do + = icon('file-text-o fw') + %span + Snippets + + - if project_nav_tab? :settings + = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do + = link_to edit_project_path(@project), title: 'Settings', data: {placement: 'right'} do + = icon('cogs fw') + %span + Settings diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml new file mode 100644 index 0000000000..857fb19995 --- /dev/null +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -0,0 +1,36 @@ +%ul.nav.nav-sidebar + = nav_link do + = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to project + + %li.separate-item + + %ul.sidebar-subnav + = nav_link(path: 'projects#edit') do + = link_to edit_project_path(@project), title: 'Project Settings', data: {placement: 'right'} do + = icon('pencil-square-o fw') + %span + Project Settings + = nav_link(controller: :deploy_keys) do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do + = icon('key fw') + %span + Deploy Keys + = nav_link(controller: :hooks) do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do + = icon('link fw') + %span + Web Hooks + = nav_link(controller: :services) do + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do + = icon('cogs fw') + %span + Services + = nav_link(controller: :protected_branches) do + = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do + = icon('lock fw') + %span + Protected Branches + diff --git a/app/views/layouts/nav/_snippets.html.haml b/app/views/layouts/nav/_snippets.html.haml new file mode 100644 index 0000000000..458b76a2c9 --- /dev/null +++ b/app/views/layouts/nav/_snippets.html.haml @@ -0,0 +1,12 @@ +%ul.nav.nav-sidebar + - if current_user + = nav_link(path: user_snippets_path(current_user), html_options: {class: 'home'}) do + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {placement: 'right'} do + = icon('dashboard fw') + %span + Your Snippets + = nav_link(path: snippets_path) do + = link_to snippets_path, title: 'Discover snippets', data: {placement: 'right'} do + = icon('globe fw') + %span + Discover Snippets diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml deleted file mode 100644 index 4d0278251a..0000000000 --- a/app/views/layouts/navless.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/head_panel", title: defined?(@title_url) ? link_to(@title, @title_url) : @title - .container.navless-container - .content - = render "layouts/flash" - = yield diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 00c7cedce4..c8662a15ad 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -27,13 +27,13 @@ } .file-stats .deleted-file { color: #B00; - }} + } %body %div.content = yield %div.footer{style: "margin-top: 10px;"} %p - \— + — %br - if @target_url #{link_to "View it on GitLab", @target_url} diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 2b5be7fc37..3193206fe1 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,6 +1,5 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: "Profile" - %body{class: "#{app_theme} profile", :'data-page' => body_data_page} - = render "layouts/head_panel", title: link_to("Profile", profile_path) - = render 'layouts/page', sidebar: 'layouts/nav/profile' +- page_title "Settings" +- header_title "Settings", profile_path +- sidebar "profile" + += render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml new file mode 100644 index 0000000000..44afa33dfe --- /dev/null +++ b/app/views/layouts/project.html.haml @@ -0,0 +1,14 @@ +- page_title @project.name_with_namespace +- header_title project_title(@project) +- sidebar "project" unless sidebar + +- content_for :scripts_body_top do + - if current_user + :javascript + window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; + window.markdown_preview_path = "#{markdown_preview_namespace_project_path(@project.namespace, @project)}"; + +- content_for :scripts_body do + = render "layouts/init_auto_complete" if current_user + += render template: "layouts/application" diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml index 0a0039dec1..4340166833 100644 --- a/app/views/layouts/project_settings.html.haml +++ b/app/views/layouts/project_settings.html.haml @@ -1,8 +1,4 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } - = render "layouts/head_panel", title: project_title(@project) - = render "layouts/init_auto_complete" - - @project_settings_nav = true - = render 'layouts/page', sidebar: 'layouts/nav/project' +- page_title "Settings" +- sidebar "project_settings" + += render template: "layouts/project" diff --git a/app/views/layouts/projects.html.haml b/app/views/layouts/projects.html.haml deleted file mode 100644 index dde0964f47..0000000000 --- a/app/views/layouts/projects.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: project_head_title - %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } - = render "layouts/head_panel", title: project_title(@project) - = render "layouts/init_auto_complete" - = render 'layouts/page', sidebar: 'layouts/nav/project' diff --git a/app/views/layouts/public_group.html.haml b/app/views/layouts/public_group.html.haml deleted file mode 100644 index b9b1d03e08..0000000000 --- a/app/views/layouts/public_group.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: group_head_title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/public_head_panel", title: link_to(@group.name, group_path(@group)) - = render 'layouts/page', sidebar: 'layouts/nav/group' diff --git a/app/views/layouts/public_projects.html.haml b/app/views/layouts/public_projects.html.haml deleted file mode 100644 index 04fa7c84e7..0000000000 --- a/app/views/layouts/public_projects.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/public_head_panel", title: project_title(@project) - = render 'layouts/page', sidebar: 'layouts/nav/project' diff --git a/app/views/layouts/public_users.html.haml b/app/views/layouts/public_users.html.haml deleted file mode 100644 index 71c16bd168..0000000000 --- a/app/views/layouts/public_users.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: @title - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/public_head_panel", title: defined?(@title_url) ? link_to(@title, @title_url) : @title - = render 'layouts/page' diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml index f9d8db06e1..fd4c7ad21a 100644 --- a/app/views/layouts/search.html.haml +++ b/app/views/layouts/search.html.haml @@ -1,10 +1,4 @@ -!!! 5 -%html{ lang: "en"} - = render "layouts/head", title: "Search" - %body{class: "#{app_theme} application", :'data-page' => body_data_page} - = render "layouts/broadcast" - = render "layouts/head_panel", title: link_to("Search", search_path) - .container.navless-container - .content - = render "layouts/flash" - = yield +- page_title "Search" +- header_title "Search", search_path + += render template: "layouts/application" diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml new file mode 100644 index 0000000000..9b0f40073a --- /dev/null +++ b/app/views/layouts/snippets.html.haml @@ -0,0 +1,5 @@ +- page_title 'Snippets' +- header_title 'Snippets', snippets_path +- sidebar "snippets" + += render template: "layouts/application" diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb index 0cc6293549..fc64c98038 100644 --- a/app/views/notify/new_issue_email.text.erb +++ b/app/views/notify/new_issue_email.text.erb @@ -1,5 +1,5 @@ New Issue was created. Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %> -Author: <%= @issue.author_name %> -Asignee: <%= @issue.assignee_name %> +Author: <%= @issue.author_name %> +Assignee: <%= @issue.assignee_name %> diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index f08039ad04..bdcca6e4ab 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -3,6 +3,6 @@ New Merge Request #<%= @merge_request.iid %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %> <%= merge_path_description(@merge_request, 'to') %> -Author: <%= @merge_request.author_name %> -Asignee: <%= @merge_request.assignee_name %> +Author: <%= @merge_request.author_name %> +Assignee: <%= @merge_request.assignee_name %> diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml index ebbe98dd47..4feacdaacf 100644 --- a/app/views/notify/new_user_email.html.haml +++ b/app/views/notify/new_user_email.html.haml @@ -11,4 +11,6 @@ - if @user.created_by_id %p - = link_to "Click here to set your password", edit_password_url(@user, :reset_password_token => @token) + = link_to "Click here to set your password", edit_password_url(@user, reset_password_token: @token) + %p + = reset_token_expire_message diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb index 96b26879a7..dd9b71e3b8 100644 --- a/app/views/notify/new_user_email.text.erb +++ b/app/views/notify/new_user_email.text.erb @@ -5,4 +5,6 @@ The Administrator created an account for you. Now you are a member of the compan login.................. <%= @user.email %> <% if @user.created_by_id %> <%= link_to "Click here to set your password", edit_password_url(@user, :reset_password_token => @token) %> + + <%= reset_token_expire_message %> <% end %> diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index a374a66233..12f83aae04 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -35,7 +35,7 @@ = diff.new_path - elsif diff.new_file %span.new-file - + + + = diff.new_path - else = diff.new_path diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml new file mode 100644 index 0000000000..c19ac429d5 --- /dev/null +++ b/app/views/profiles/_event_table.html.haml @@ -0,0 +1,16 @@ +%table.table#audits + %thead + %tr + %th Action + %th When + + %tbody + - events.each do |event| + %tr + %td + %span + Signed in with + %b= event.details[:with] + authentication + %td #{time_ago_in_words event.created_at} ago += paginate events, theme: "gitlab" diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 5bffb4acc1..767fe2e0e9 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,12 +1,18 @@ +- page_title "Account" +%h3.page-title + = page_title +%p.light + Change your username and basic account settings. +%hr - if current_user.ldap_user? .alert.alert-info Some options are unavailable for LDAP accounts .account-page - %fieldset.update-token - %legend + .panel.panel-default.update-token + .panel-heading Reset Private token - %div + .panel-body = form_for @user, url: reset_private_token_profile_path, method: :put do |f| .data %p @@ -20,58 +26,91 @@ - if current_user.private_token = text_field_tag "token", current_user.private_token, class: "form-control" %div - = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-primary btn-build-token" + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" - else %span You don`t have one yet. Click generate to fix it. - = f.submit 'Generate', class: "btn success btn-build-token" + = f.submit 'Generate', class: "btn btn-default btn-build-token" - - - if show_profile_social_tab? - %fieldset - %legend Connected Accounts - .oauth-buttons.append-bottom-10 - %p Click on icon to activate signin with one of the following services - - enabled_social_providers.each do |provider| - .btn-group - = link_to oauth_image_tag(provider), omniauth_authorize_path(User, provider), - class: "btn btn-lg #{'active' if oauth_active?(provider)}" - - if oauth_active?(provider) - = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do - %i.fa.fa-close - - - if show_profile_username_tab? - %fieldset.update-username - %legend - Change Username - = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| - %p - Changing your username will change path to all personal projects! - %div - = f.text_field :username, required: true, class: 'form-control' -   - .loading-gif.hide + - unless current_user.ldap_user? + .panel.panel-default + .panel-heading + Two-factor Authentication + .panel-body + - if current_user.two_factor_enabled? + .pull-right + = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', + data: { confirm: 'Are you sure?' } + %p.text-success + %strong + Two-factor Authentication is enabled %p - %i.fa.fa-spinner.fa-spin - Saving new username - %p.light - = user_url(@user) - %div - = f.submit 'Save username', class: "btn btn-warning" + If you lose your recovery codes you can + %strong + = succeed ',' do + = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' } + invalidating all previous codes. - - if show_profile_remove_tab? - %fieldset.remove-account - %legend + - else + %p + Increase your account's security by enabling two-factor authentication (2FA). + %p + Each time you log in you’ll be required to provide your username and + password as usual, plus a randomly-generated code from your phone. + %div + = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' + + - if button_based_providers.any? + .panel.panel-default + .panel-heading + Connected Accounts + .panel-body + .oauth-buttons.append-bottom-10 + %p Click on icon to activate signin with one of the following services + - button_based_providers.each do |provider| + .btn-group + = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true" + + - if auth_active?(provider) + = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do + = icon('close') + + - if current_user.can_change_username? + .panel.panel-warning.update-username + .panel-heading + Change Username + .panel-body + = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| + %p + Changing your username will change path to all personal projects! + %div + = f.text_field :username, required: true, class: 'form-control' +   + .loading-gif.hide + %p + = icon('spinner spin') + Saving new username + %p.light + = user_url(@user) + %div + = f.submit 'Save username', class: "btn btn-warning" + + - if signup_enabled? + .panel.panel-danger.remove-account + .panel-heading Remove account - %div - %p Deleting an account has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = current_user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - - if current_user.solo_owned_groups.present? - %li - The following groups will be abandoned. You should transfer or remove them: - %strong #{current_user.solo_owned_groups.map(&:name).join(', ')} - = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" - + .panel-body + - if @user.can_be_removed? + %p Deleting an account has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = current_user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" + - else + - if @user.solo_owned_groups.present? + %p + Your account is currently an owner in these groups: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %p + You must transfer ownership or delete these groups before you can delete your account. diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index 97e98948f3..3a3e6e1b1c 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -1,33 +1,44 @@ +- page_title "Applications" %h3.page-title - Application Settings + = page_title %p.light - OAuth2 protocol settings below. + - if user_oauth_applications? + Manage applications that can use GitLab as an OAuth provider, + and applications that you've authorized to use your account. + - else + Manage applications that you've authorized to use your account. +%hr -%fieldset.oauth-applications - %legend Your applications - %p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' - - if @applications.any? - %table.table.table-striped - %thead - %tr - %th Name - %th Callback URL - %th Clients - %th - %th - %tbody - - @applications.each do |application| - %tr{:id => "application_#{application.id}"} - %td= link_to application.name, oauth_application_path(application) - %td - - application.redirect_uri.split.each do |uri| - %div= uri - %td= application.access_tokens.count - %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' - %td= render 'doorkeeper/applications/delete_form', application: application +- if user_oauth_applications? + .oauth-applications + %h3 + Your applications + .pull-right + = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' + - if @applications.any? + %table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th Clients + %th + %th + %tbody + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, oauth_application_path(application) + %td + - application.redirect_uri.split.each do |uri| + %div= uri + %td= application.access_tokens.count + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' + %td= render 'doorkeeper/applications/delete_form', application: application -%fieldset.oauth-authorized-applications.prepend-top-20 - %legend Authorized applications +.oauth-authorized-applications.prepend-top-20 + - if user_oauth_applications? + %h3 + Authorized applications - if @authorized_tokens.any? %table.table.table-striped @@ -45,5 +56,14 @@ %td= token.created_at %td= token.scopes %td= render 'doorkeeper/authorized_applications/delete_form', application: app + - @authorized_anonymous_tokens.each do |token| + %tr + %td + Anonymous + %div.help-block + %em Authorization was granted by entering your username and password in the application. + %td= token.created_at + %td= token.scopes + %td= render 'doorkeeper/authorized_applications/delete_form', token: token - else - %p.light You dont have any authorized applications + %p.light You don't have any authorized applications diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml new file mode 100644 index 0000000000..698d603742 --- /dev/null +++ b/app/views/profiles/audit_log.html.haml @@ -0,0 +1,5 @@ +- page_title "Audit Log" +%h3.page-title Audit Log +%p.light History of authentications + += render 'event_table', events: @events \ No newline at end of file diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml deleted file mode 100644 index cc00d08d03..0000000000 --- a/app/views/profiles/design.html.haml +++ /dev/null @@ -1,53 +0,0 @@ -%h3.page-title - Design Settings -%p.light - Appearance settings will be saved to your profile and made available across all devices. -%hr - -= form_for @user, url: profile_path, remote: true, method: :put do |f| - %fieldset.application-theme - %legend - Application theme - .themes_opts - = label_tag do - .prev.default - = f.radio_button :theme_id, 1 - Default - - = label_tag do - .prev.classic - = f.radio_button :theme_id, 2 - Classic - - = label_tag do - .prev.modern - = f.radio_button :theme_id, 3 - Modern - - = label_tag do - .prev.gray - = f.radio_button :theme_id, 4 - Gray - - = label_tag do - .prev.violet - = f.radio_button :theme_id, 5 - Violet - - = label_tag do - .prev.blue - = f.radio_button :theme_id, 6 - Blue - %br - .clearfix - - %fieldset.code-preview-theme - %legend - Code preview theme - .code_highlight_opts - - color_schemes.each do |color_scheme_id, color_scheme| - = label_tag do - .prev - = image_tag "#{color_scheme}-scheme-preview.png" - = f.radio_button :color_scheme_id, color_scheme_id - = color_scheme.gsub(/[-_]+/, ' ').humanize diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 09f290429e..66812872c4 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,18 +1,27 @@ +- page_title "Emails" %h3.page-title - Email Settings + = page_title %p.light - Your - %b Primary Email - will be used for avatar detection and web based operations, such as edits and merges. - %br - Your - %b Notification Email - will be used for account notifications. - %br - All email addresses will be used to identify your commits. - + Control emails linked to your account %hr + +%ul + %li + Your + %b Primary Email + will be used for avatar detection and web based operations, such as edits and merges. + %li + Your + %b Notification Email + will be used for account notifications. + %li + Your + %b Public Email + will be displayed on your public profile. + %li + All email addresses will be used to identify your commits. + .panel.panel-default .panel-heading Emails (#{@emails.count + 1}) @@ -20,13 +29,17 @@ %li %strong= @primary %span.label.label-success Primary Email - - if @primary === @public_email + - if @primary === current_user.public_email %span.label.label-info Public Email + - if @primary === current_user.notification_email + %span.label.label-info Notification Email - @emails.each do |email| %li %strong= email.email - - if email.email === @public_email + - if email.email === current_user.public_email %span.label.label-info Public Email + - if email.email === current_user.notification_email + %span.label.label-info Notification Email %span.cgray added #{time_ago_with_tooltip(email.created_at)} = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right' diff --git a/app/views/profiles/history.html.haml b/app/views/profiles/history.html.haml deleted file mode 100644 index b1ab433f48..0000000000 --- a/app/views/profiles/history.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%h3.page-title - Your Account History -%p.light - All events created by your account are listed below. -%hr -.profile_history - = render @events -%hr -= paginate @events, theme: "gitlab" - diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index f905417f0e..b76a5b636a 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -6,14 +6,13 @@ - @key.errors.full_messages.each do |msg| %li= msg - .form-group - = f.label :title, class: 'control-label' - .col-sm-10= f.text_field :title, class: "form-control" .form-group = f.label :key, class: 'control-label' .col-sm-10 = f.text_area :key, class: "form-control", rows: 8 - + .form-group + = f.label :title, class: 'control-label' + .col-sm-10= f.text_field :title, class: "form-control" .form-actions = f.submit 'Add key', class: "btn btn-create" diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index fe5770f45c..9bbccbc45e 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -3,8 +3,7 @@ = link_to path_to_key(key, is_admin) do %strong= key.title %td - %span - (#{key.fingerprint}) + %code.key-fingerprint= key.fingerprint %td %span.cgray added #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 8bac22a2e1..e0ae4d9720 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -15,7 +15,7 @@ .col-md-8 %p %span.light Fingerprint: - %strong= @key.fingerprint + %code.key-fingerprint= @key.fingerprint %pre.well-pre = @key.key .pull-right diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 0904c50c88..06655f7ba3 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,5 +1,6 @@ +- page_title "SSH Keys" %h3.page-title - SSH Keys Settings + = page_title .pull-right = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" %p.light diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml index ccec716d0c..2bf207a322 100644 --- a/app/views/profiles/keys/new.html.haml +++ b/app/views/profiles/keys/new.html.haml @@ -1,3 +1,4 @@ +- page_title "Add SSH Keys" %h3.page-title Add an SSH Key %p.light Paste your public key here. Read more about how to generate a key on #{link_to "the SSH help page", help_page_path("ssh", "README")}. diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml index cfd5329896..89f6f01581 100644 --- a/app/views/profiles/keys/show.html.haml +++ b/app/views/profiles/keys/show.html.haml @@ -1 +1,2 @@ +- page_title @key.title, "SSH Keys" = render "key_details" diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 273e72f8a4..9480a19f5b 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,5 +1,6 @@ +- page_title "Notifications" %h3.page-title - Notifications Settings + = page_title %p.light These are your global notification settings. %hr diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 4b04b113e8..399ae98adf 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,4 +1,6 @@ -%h3.page-title Password Settings +- page_title "Password" +%h3.page-title + = page_title %p.light - if @user.password_automatically_set? Set your password. diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index 8bed6e0dbe..9c6204963e 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -1,3 +1,5 @@ +- page_title "New Password" +- header_title "New Password" %h3.page-title Setup new password %hr = form_for @user, url: profile_password_path, method: :post, html: { class: 'form-horizontal '} do |f| diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml new file mode 100644 index 0000000000..1134317ee0 --- /dev/null +++ b/app/views/profiles/preferences/show.html.haml @@ -0,0 +1,50 @@ +- page_title 'Preferences' +%h3.page-title + = page_title +%p.light + These settings allow you to customize the appearance and behavior of the site. + They are saved with your account and will persist to any device you use to + access the site. +%hr + += form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f| + .panel.panel-default.application-theme + .panel-heading + Application theme + .panel-body + - Gitlab::Themes.each do |theme| + = label_tag do + .preview{class: theme.css_class} + = f.radio_button :theme_id, theme.id + = theme.name + + .panel.panel-default.syntax-theme + .panel-heading + Syntax highlighting theme + .panel-body + - color_schemes.each do |color_scheme_id, color_scheme| + = label_tag do + .preview= image_tag "#{color_scheme}-scheme-preview.png" + = f.radio_button :color_scheme_id, color_scheme_id + = color_scheme.tr('-_', ' ').titleize + + .panel.panel-default + .panel-heading + Behavior + .panel-body + .form-group + = f.label :dashboard, class: 'control-label' do + Default Dashboard + = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank') + .col-sm-10 + = f.select :dashboard, dashboard_choices, {}, class: 'form-control' + .form-group + = f.label :project_view, class: 'control-label' do + Project view + = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank') + .col-sm-10 + = f.select :project_view, project_view_choices, {}, class: 'form-control' + .help-block + Choose what content you want to see when visit project page + .panel-footer + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb new file mode 100644 index 0000000000..6c4b0ce757 --- /dev/null +++ b/app/views/profiles/preferences/update.js.erb @@ -0,0 +1,9 @@ +// Remove body class for any previous theme, re-add current one +$('body').removeClass('<%= Gitlab::Themes.body_classes %>') +$('body').addClass('<%= user_application_theme %>') + +// Re-enable the "Save" button +$('input[type=submit]').enable() + +// Show the notice flash message +new Flash('<%= flash.discard(:notice) %>', 'notice') diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 6c745e69e4..9fdeddfcc7 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,5 +1,6 @@ +- page_title "Profile" %h3.page-title - Profile Settings + = page_title %p.light This information will appear on your profile. - if current_user.ldap_user? @@ -36,8 +37,11 @@ = f.text_field :email, class: "form-control", required: true - if @user.unconfirmed_email.present? %span.help-block - Please click the link in the confirmation email before continuing, it was sent to - %strong #{@user.unconfirmed_email} + Please click the link in the confirmation email before continuing. It was sent to + = succeed "." do + %strong #{@user.unconfirmed_email} + %p + = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post - else %span.help-block We also use email for avatar detection if no avatar is uploaded. @@ -78,12 +82,12 @@ You can change your avatar here - if Gitlab.config.gravatar.enabled %br - or remove the current avatar to revert to #{link_to "gravatar.com", "http://gravatar.com"} + or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} - else You can upload an avatar here - if Gitlab.config.gravatar.enabled %br - or change it at #{link_to "gravatar.com", "http://gravatar.com"} + or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} %hr %a.choose-btn.btn.btn-sm.js-choose-user-avatar-button %i.fa.fa-paperclip @@ -105,6 +109,5 @@ .row .col-md-7 .form-group - .col-sm-2   - .col-sm-10 + .col-sm-offset-2.col-sm-10 = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml new file mode 100644 index 0000000000..43b58be7f9 --- /dev/null +++ b/app/views/profiles/two_factor_auths/_codes.html.haml @@ -0,0 +1,13 @@ +%p.slead + Should you ever lose your phone, each of these recovery codes can be used one + time each to regain access to your account. Please save them in a safe place, or you + %b will + lose access to your account. + +.codes.well + %ul + - @codes.each do |code| + %li + %span.monospace= code + += link_to 'Proceed', profile_account_path, class: 'btn btn-success' diff --git a/app/views/profiles/two_factor_auths/codes.html.haml b/app/views/profiles/two_factor_auths/codes.html.haml new file mode 100644 index 0000000000..addf356697 --- /dev/null +++ b/app/views/profiles/two_factor_auths/codes.html.haml @@ -0,0 +1,5 @@ +- page_title 'Recovery Codes', 'Two-factor Authentication' + +%h3.page-title Two-factor Authentication Recovery codes +%hr += render 'codes' diff --git a/app/views/profiles/two_factor_auths/create.html.haml b/app/views/profiles/two_factor_auths/create.html.haml new file mode 100644 index 0000000000..e330aadac1 --- /dev/null +++ b/app/views/profiles/two_factor_auths/create.html.haml @@ -0,0 +1,6 @@ +- page_title 'Two-factor Authentication', 'Account' + +.alert.alert-success + Congratulations! You have enabled Two-factor Authentication! + += render 'codes' diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml new file mode 100644 index 0000000000..92dc58c10d --- /dev/null +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -0,0 +1,40 @@ +- page_title 'Two-factor Authentication', 'Account' + +%h2.page-title Two-Factor Authentication (2FA) +%p + Download the Google Authenticator application from App Store for iOS or Google + Play for Android and scan this code. + + More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. + +%hr + += form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal two-factor-new' do |f| + - if @error + .alert.alert-danger + = @error + .form-group + .col-lg-2.col-lg-offset-2 + = raw @qr_code + .col-lg-7.col-lg-offset-1.manual-instructions + %h3 Can't scan the code? + + %p + To add the entry manually, provide the following details to the + application on your phone. + + %dl + %dt Account + %dd= current_user.email + %dl + %dt Key + %dd= current_user.otp_secret.scan(/.{4}/).join(' ') + %dl + %dt Time based + %dd Yes + .form-group + = label_tag :pin_code, nil, class: "control-label" + .col-lg-10 + = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true + .form-actions + = submit_tag 'Submit', class: 'btn btn-success' diff --git a/app/views/profiles/update.js.erb b/app/views/profiles/update.js.erb deleted file mode 100644 index e664ac2a52..0000000000 --- a/app/views/profiles/update.js.erb +++ /dev/null @@ -1,9 +0,0 @@ -// Remove body class for any previous theme, re-add current one -$('body').removeClass('ui_basic ui_mars ui_modern ui_gray ui_color light_theme dark_theme') -$('body').addClass('<%= app_theme %> <%= theme_type %>') - -// Re-render the header to reflect the new theme -$('header').html('<%= escape_javascript(render("layouts/head_panel", title: "Profile")) %>') - -// Re-initialize header tooltips -$('.has_bottom_tooltip').tooltip({placement: 'bottom'}) diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml new file mode 100644 index 0000000000..ee02b7f6a6 --- /dev/null +++ b/app/views/projects/_activity.html.haml @@ -0,0 +1,15 @@ += render 'projects/last_push' +.hidden-xs + - if current_user + %ul.nav.nav-pills.event_filter.pull-right + %li + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'rss-btn' do + %i.fa.fa-rss + + = render 'shared/event_filter' + %hr +.content_list{:"data-href" => activity_project_path(@project)} += spinner + +:coffeescript + new Activities() diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index 07d4d60276..745163e79a 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -1,4 +1,4 @@ -%div#bitbucket_import_modal.modal.hide +%div#bitbucket_import_modal.modal .modal-dialog .modal-content .modal-header diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml deleted file mode 100644 index 3036f11bb2..0000000000 --- a/app/views/projects/_dropdown.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -- if current_user - .dropdown.pull-right - %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-bars - %ul.dropdown-menu - - if @project.issues_enabled && can?(current_user, :write_issue, @project) - %li - = link_to url_for_new_issue(@project, only_path: true), title: "New Issue" do - %i.fa.fa-fw.fa-exclamation-circle - New issue - - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) - %li - = link_to new_namespace_project_merge_request_path(@project.namespace, @project), title: "New Merge Request" do - %i.fa.fa-fw.fa-tasks - New merge request - - if @project.snippets_enabled && can?(current_user, :write_snippet, @project) - %li - = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do - %i.fa.fa-fw.fa-file-text-o - New snippet - - if can?(current_user, :admin_project_member, @project) - %li - = link_to namespace_project_project_members_path(@project.namespace, @project), title: "New project member" do - %i.fa.fa-fw.fa-users - New project member - - if can? current_user, :push_code, @project - %li.divider - %li - = link_to new_namespace_project_branch_path(@project.namespace, @project) do - %i.fa.fa-fw.fa-code-fork - New branch - %li - = link_to new_namespace_project_tag_path(@project.namespace, @project) do - %i.fa.fa-fw.fa-tag - New tag - - diff --git a/app/views/projects/_github_import_modal.html.haml b/app/views/projects/_github_import_modal.html.haml index e88a0f7d68..de58b27df2 100644 --- a/app/views/projects/_github_import_modal.html.haml +++ b/app/views/projects/_github_import_modal.html.haml @@ -1,4 +1,4 @@ -%div#github_import_modal.modal.hide +%div#github_import_modal.modal .modal-dialog .modal-content .modal-header diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml index 52212b6ae0..ae6c25f937 100644 --- a/app/views/projects/_gitlab_import_modal.html.haml +++ b/app/views/projects/_gitlab_import_modal.html.haml @@ -1,4 +1,4 @@ -%div#gitlab_import_modal.modal.hide +%div#gitlab_import_modal.modal .modal-dialog .modal-content .modal-header diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 5689bdee1c..bec40ec27a 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,43 +1,30 @@ - empty_repo = @project.empty_repo? -.project-home-panel{:class => ("empty-project" if empty_repo)} +.project-home-panel.clearfix{:class => ("empty-project" if empty_repo)} .project-identicon-holder - = project_icon(@project, alt: '', class: 'avatar project-avatar') - .project-home-row.project-home-row-top - .project-home-desc - - if @project.description.present? - = escaped_autolink(@project.description) - - if can?(current_user, :admin_project, @project) - – - = link_to 'Edit', edit_namespace_project_path - - elsif !@project.empty_repo? && @repository.readme - - readme = @repository.readme - – - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do - = readme.name - .project-repo-buttons - .inline.star.js-toggler-container{class: @show_star ? 'on' : ''} - - if current_user - = link_to_toggle_star('Star this project.', false) - = link_to_toggle_star('Unstar this project.', true) - - else - = link_to new_user_session_path, class: 'btn star-btn has_tooltip', title: 'You must sign in to star a project' do - %span - = icon('star') - Star - %span.count - = @project.star_count - - unless @project.empty_repo? - - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - .inline.fork-buttons.prepend-left-10 - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-sm btn-default' do - = link_to_toggle_fork - - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-sm btn-default' do - = link_to_toggle_fork + = project_icon(@project, alt: '', class: 'project-avatar avatar s90') + .project-home-desc.lead + %h1= @project.name + - if @project.description.present? + = markdown(@project.description, pipeline: :description) - .project-home-row.hidden-xs - - if current_user && !empty_repo - .project-home-dropdown - = render "dropdown" - = render "shared/clone_panel" + + .project-repo-buttons + = render 'projects/buttons/star' + + - unless empty_repo + = render 'projects/buttons/fork' + + - if forked_from_project = @project.forked_from_project + = link_to project_path(forked_from_project), class: 'btn' do + = icon("code-fork fw") + Forked from + = forked_from_project.namespace.try(:name) + + - if can? current_user, :download_code, @project + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = icon('download fw') + Download + + = render 'projects/buttons/dropdown' + + = render "shared/clone_panel" diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml deleted file mode 100644 index e321a84974..0000000000 --- a/app/views/projects/_issuable_form.html.haml +++ /dev/null @@ -1,87 +0,0 @@ -- if issuable.errors.any? - .row - .col-sm-10.col-sm-offset-2 - .alert.alert-danger - - issuable.errors.full_messages.each do |msg| - %span= msg - %br -.form-group - = f.label :title, class: 'control-label' do - %strong= 'Title *' - .col-sm-10 - = f.text_field :title, maxlength: 255, autofocus: true, - class: 'form-control pad js-gfm-input', required: true -.form-group.issuable-description - = f.label :description, 'Description', class: 'control-label' - .col-sm-10 - - = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do - = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' - .col-sm-12.hint - .pull-left - Parsed with - #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. - .pull-right - Attach files by dragging & dropping - or #{link_to 'selecting them', '#', class: 'markdown-selector' }. - - .clearfix - .error-alert -%hr -.form-group - .issue-assignee - = f.label :assignee_id, class: 'control-label' do - %i.fa.fa-user - Assign to - .col-sm-10 - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select a user', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id) -   - = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' -.form-group - .issue-milestone - = f.label :milestone_id, class: 'control-label' do - %i.fa.fa-clock-o - Milestone - .col-sm-10 - - if milestone_options(issuable).present? - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: 'Select milestone' }, { class: 'select2' }) - - else - .prepend-top-10 - %span.light No open milestones available. -   - - if can? current_user, :admin_milestone, issuable.project - = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank -.form-group - = f.label :label_ids, class: 'control-label' do - %i.fa.fa-tag - Labels - .col-sm-10 - - if issuable.project.labels.any? - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2' - - else - .prepend-top-10 - %span.light No labels yet. -   - - if can? current_user, :admin_label, issuable.project - = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank - -.form-actions - - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? - %p - Please review the - %strong #{link_to 'guidelines for contribution', guide_url} - to this repository. - - if issuable.new_record? - = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - - else - = f.submit 'Save changes', class: 'btn btn-save' - - if issuable.new_record? - - cancel_project = issuable.source_project - - else - - cancel_project = issuable.project - = link_to 'Cancel', [cancel_project.namespace.becomes(Namespace), cancel_project, issuable], class: 'btn btn-cancel' diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml new file mode 100644 index 0000000000..30622d8a91 --- /dev/null +++ b/app/views/projects/_last_push.html.haml @@ -0,0 +1,14 @@ +- if event = last_push_event + - if show_last_push_widget?(event) + .hidden-xs.center + .slead + %span You pushed to + = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do + %strong= event.ref_name + branch + #{time_ago_with_tooltip(event.created_at)} + + %div + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do + Create Merge Request + %hr diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index f356a25dbf..b7bca6dae0 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,13 +1,24 @@ -%ul.nav.nav-tabs - %li.active - = link_to '#md-write-holder', class: 'js-md-write-button' do - Write - %li - = link_to '#md-preview-holder', class: 'js-md-preview-button', - data: { url: markdown_preview_namespace_project_path(@project.namespace, @project) } do - Preview -%div - .md-write-holder - = yield - .md-preview-holder.hide - .js-md-preview{class: (preview_class if defined?(preview_class))} +.md-area + .md-header.clearfix + %ul.nav.nav-tabs + %li.active + = link_to '#md-write-holder', class: 'js-md-write-button', tabindex: '-1' do + Write + %li + = link_to '#md-preview-holder', class: 'js-md-preview-button', tabindex: '-1' do + Preview + + - if defined?(referenced_users) && referenced_users + %span.referenced-users.pull-left.hide + = icon('exclamation-triangle') + You are about to add + %strong + %span.js-referenced-users-count 0 + people + to the discussion. Proceed with caution. + + %div + .md-write-holder + = yield + .md.md-preview-holder.hide + .js-md-preview{class: (preview_class if defined?(preview_class))} diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml new file mode 100644 index 0000000000..5038edb95e --- /dev/null +++ b/app/views/projects/_readme.html.haml @@ -0,0 +1,24 @@ +- if readme = @repository.readme + %article.readme-holder#README + .clearfix + .pull-right +   + - if can?(current_user, :push_code, @project) + = link_to namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do + %i.fa.fa-pencil + .wiki + = cache(readme_cache_key) do + = render_readme(readme) +- else + %h3.page-title + This project does not have README yet + - if can?(current_user, :push_code, @project) + %p.slead + A + %code README + file contains information about other files in a repository and is commonly + distributed with computer software, forming part of its documentation. + %br + We recommend you to + = link_to "add README", new_readme_path, class: 'underlined-link' + file to the repository and GitLab will render it here instead of this message. diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml deleted file mode 100644 index 281a84a3d3..0000000000 --- a/app/views/projects/_settings_nav.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -%ul.project-settings-nav.sidebar-subnav - = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), title: 'Project', class: "stat-tab tab " do - %i.fa.fa-pencil-square-o - %span - Project - = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: "team-tab tab" do - %i.fa.fa-users - %span - Members - = nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do - %i.fa.fa-key - %span - Deploy Keys - = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do - %i.fa.fa-link - %span - Web Hooks - = nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do - %i.fa.fa-cogs - %span - Services - = nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do - %i.fa.fa-lock - %span - Protected branches diff --git a/app/views/projects/_visibility_level.html.haml b/app/views/projects/_visibility_level.html.haml deleted file mode 100644 index 42c8e68522..0000000000 --- a/app/views/projects/_visibility_level.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -.form-group.project-visibility-level-holder - = f.label :visibility_level, class: 'control-label' do - Visibility Level - = link_to "(?)", help_page_path("public_access", "public_access") - .col-sm-10 - - if can_change_visibility_level - - Gitlab::VisibilityLevel.values.each do |level| - .radio - - restricted = restricted_visibility_levels.include?(level) - = label :project_visibility_level, level do - = f.radio_button :visibility_level, level, checked: (visibility_level == level), disabled: restricted - = visibility_level_icon(level) - .option-title - = visibility_level_label(level) - .option-descr - = visibility_level_description(level) - - unless restricted_visibility_levels.empty? - .col-sm-10 - %span.info - Some visibility level settings have been restricted by the administrator. - - else - .col-sm-10 - %span.info - = visibility_level_icon(visibility_level) - %strong - = visibility_level_label(visibility_level) - .light= visibility_level_description(visibility_level) diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index cf1c55ecca..6a41cdbc90 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -2,7 +2,7 @@ %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } .zen-backdrop - classes << ' js-gfm-input markdown-area' - = f.text_area attr, class: classes, placeholder: 'Leave a comment' + = f.text_area attr, class: classes, placeholder: '' = link_to nil, class: 'zen-enter-link', tabindex: '-1' do %i.fa.fa-expand Edit in fullscreen diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml new file mode 100644 index 0000000000..65674913bb --- /dev/null +++ b/app/views/projects/activity.html.haml @@ -0,0 +1 @@ += render 'projects/activity' diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index e6a859fea8..a3ff7ce2f1 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Blame", @blob.path, @ref %h3.page-title Blame view #tree-holder.tree-holder @@ -11,25 +12,33 @@ = render "projects/blob/actions" .file-content.blame.highlight %table - - @blame.each do |commit, lines, since| - - commit = Commit.new(commit) + - current_line = 1 + - @blame.each do |blame_group| %tr %td.blame-commit - %span.commit - = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "commit_short_id" -   - = commit_author_link(commit, avatar: true, size: 16) -   - = link_to_gfm truncate(commit.title, length: 20), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "row_title" + .commit + - commit = Commit.new(blame_group[:commit], @project) + .commit-row-title + %strong + = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" + .pull-right + = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace" +   + .light + = commit_author_link(commit, avatar: false) + authored + #{time_ago_with_tooltip(commit.committed_date)} %td.lines.blame-numbers %pre - - (since...(since + lines.count)).each do |i| + - line_count = blame_group[:lines].count + - (current_line...(current_line + line_count)).each do |i| = i \ + - current_line += line_count %td.lines %pre{class: 'code highlight white'} %code - :erb - <% lines.each do |line| %> - <%= highlight(@blob.name, line, true).html_safe %> - <% end %> + - blame_group[:lines].each do |line| + :erb + <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> + diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index 09559a4967..cae5ff0109 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -1,4 +1,4 @@ -#modal-remove-blob.modal.hide +#modal-remove-blob.modal .modal-dialog .modal-content .modal-header @@ -9,14 +9,10 @@ %strong= @ref .modal-body - = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal' do + = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-requires-input' do = render 'shared/commit_message_container', params: params, placeholder: 'Removed this file because...' .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 = button_tag 'Remove file', class: 'btn btn-remove btn-remove-file' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" - -:javascript - disableButtonIfEmptyField('#commit_message', '.btn-remove-file') diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index f6bd62f239..4429c395ae 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -1,8 +1,4 @@ -- if gitlab_markdown?(blob.name) - .file-content.wiki - = preserve do - = markdown(blob.data) -- elsif markup?(blob.name) +- if markup?(blob.name) .file-content.wiki = render_markup(blob.name, blob.data) - else diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 5c79d0ef11..f3b01ff328 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -2,7 +2,7 @@ - if @form.unfold? && @form.since != 1 && !@form.bottom? %tr.line_holder{ id: @form.since } = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.since, line_new: @form.since, bottom: false} + line_old: @form.since, line_new: @form.since, bottom: false, new_file: false} - @lines.each_with_index do |line, index| - line_new = index + @form.since @@ -11,9 +11,9 @@ %td.old_line.diff-line-num{data: {linenumber: line_old}} = link_to raw(line_old), "#" %td.new_line= link_to raw(line_new) , "#" - %td.line_content.noteable_line= line + %td.line_content.noteable_line= ' ' * @form.indent + line - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.to, line_new: @form.to, bottom: true} + line_old: @form.to, line_new: @form.to, bottom: true, new_file: false} diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 1f61a0b940..a12cd660fc 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @blob.path, @ref .file-editor %ul.nav.nav-tabs.js-edit-mode %li.active @@ -10,10 +11,9 @@ %i.fa.fa-eye = editing_preview_title(@blob.name) - = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: "form-horizontal") do + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input') do = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data - = render 'shared/commit_message_container', params: params, - placeholder: "Update #{@blob.name}" + = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" .form-group.branch = label_tag 'branch', class: 'control-label' do @@ -24,8 +24,7 @@ = hidden_field_tag 'last_commit', @last_commit = hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] - = render 'projects/commit_button', ref: @ref, - cancel_path: @after_edit_path + = render 'projects/commit_button', ref: @ref, cancel_path: @after_edit_path :javascript blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index d78a01f642..dac984f8c3 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,6 +1,7 @@ +- page_title "New File", @ref %h3.page-title New file .file-editor - = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file') do + = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do = render 'projects/blob/editor', ref: @ref = render 'shared/commit_message_container', params: params, placeholder: 'Add new file' diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 69167654c3..bd2fc43633 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -1,3 +1,7 @@ +- page_title @blob.path, @ref + += render 'projects/last_push' + %div.tree-ref-holder = render 'shared/ref_switcher', destination: 'blob', path: @path diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 4e7415be4a..a693c4b282 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -5,21 +5,29 @@ %strong.str-truncated= branch.name - if branch.name == @repository.root_ref %span.label.label-info default + - elsif @repository.merged_to_root_ref? branch.name + %span.label.label-primary.has_tooltip(title="Merged into #{@repository.root_ref}") + %i.fa.fa-check + merged + - if @project.protected_branch? branch.name %span.label.label-success %i.fa.fa-lock protected .pull-right - - if can?(current_user, :download_code, @project) - = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-xs' + - if create_mr_button?(@repository.root_ref, branch.name) + = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do + = icon('plus') + Merge Request + - if branch.name != @repository.root_ref = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do - %i.fa.fa-files-o + = icon("exchange") Compare - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do - %i.fa.fa-trash-o + = icon("trash-o") - if commit %ul.list-unstyled diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index a313ffcf27..80acc93790 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Branches" = render "projects/commits/head" %h3.page-title Branches diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index e5fcb98c68..29e82b9388 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Branch" - if @error .alert.alert-danger %button{ type: "button", class: "close", "data-dismiss" => "alert"} × @@ -5,7 +6,7 @@ %h3.page-title %i.fa.fa-code-fork New branch -= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal" do += form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-requires-input" do .form-group = label_tag :branch_name, 'Name for new branch', class: 'control-label' .col-sm-10 @@ -19,7 +20,6 @@ = link_to 'Cancel', namespace_project_branches_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript - disableButtonIfAnyEmptyField($("#new-branch-form"), ".form-control", ".btn-create"); var availableTags = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml new file mode 100644 index 0000000000..cade930c8c --- /dev/null +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -0,0 +1,32 @@ +- if current_user + %span.dropdown + %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"} + = icon('plus') + %ul.dropdown-menu + - if can?(current_user, :create_issue, @project) + %li + = link_to url_for_new_issue do + = icon('exclamation-circle fw') + New issue + - if can?(current_user, :create_merge_request, @project) + %li + = link_to new_namespace_project_merge_request_path(@project.namespace, @project) do + = icon('tasks fw') + New merge request + - if can?(current_user, :create_snippet, @project) + %li + = link_to new_namespace_project_snippet_path(@project.namespace, @project) do + = icon('file-text-o fw') + New snippet + - if can?(current_user, :push_code, @project) + %li.divider + %li + = link_to new_namespace_project_branch_path(@project.namespace, @project) do + = icon('code-fork fw') + New branch + %li + = link_to new_namespace_project_tag_path(@project.namespace, @project) do + = icon('tags fw') + New tag + + diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml new file mode 100644 index 0000000000..854c154824 --- /dev/null +++ b/app/views/projects/buttons/_fork.html.haml @@ -0,0 +1,13 @@ +- if current_user && can?(current_user, :fork_project, @project) + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn' do + = icon('code-fork fw') + Fork + %span.count + = @project.forks_count + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do + = icon('code-fork fw') + Fork + %span.count + = @project.forks_count diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml new file mode 100644 index 0000000000..5d7df5ae09 --- /dev/null +++ b/app/views/projects/buttons/_star.html.haml @@ -0,0 +1,22 @@ +- if current_user + = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do + = icon('star fw') + - if current_user.starred?(@project) + Unstar + - else + Star + %span.count + = @project.star_count + + :coffeescript + $('.project-home-panel .toggle-star').on 'ajax:success', (e, data, status, xhr) -> + $(@).replaceWith(data.html) + .on 'ajax:error', (e, xhr, status, error) -> + new Flash('Star toggle failed. Try again later.', 'alert') + +- else + = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do + = icon('star fw') + Star + %span.count + = @project.star_count diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index fc721067ed..60b112e67d 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,3 +1,4 @@ +- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" = render "commit_box" = render "projects/diffs/diffs", diffs: @diffs, project: @project -= render "projects/notes/notes_with_form" += render "projects/notes/notes_with_form", view: params[:view] diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index c6026f9680..74f8d8b15c 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,33 +1,34 @@ -%li.commit.js-toggle-container - .commit-row-title - %strong.str-truncated - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - - if commit.description? - %a.text-expander.js-toggle-button ... +- if @note_counts + - note_count = @note_counts.fetch(commit.id, 0) +- else + - notes = commit.notes + - note_count = notes.user.count - .pull-right - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" += cache [project.id, commit.id, note_count] do + %li.commit.js-toggle-container + .commit-row-title + %strong.str-truncated + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" + - if commit.description? + %a.text-expander.js-toggle-button ... - .notes_count - - if @note_counts - - note_count = @note_counts.fetch(commit.id, 0) - - else - - notes = project.notes.for_commit_id(commit.id) - - note_count = notes.user.count + .pull-right + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" - - if note_count > 0 - %span.light - %i.fa.fa-comments - = note_count + .notes_count + - if note_count > 0 + %span.light + %i.fa.fa-comments + = note_count - - if commit.description? - .commit-row-description.js-toggle-content - %pre - = preserve(gfm(escape_once(commit.description))) + - if commit.description? + .commit-row-description.js-toggle-content + %pre + = preserve(gfm(escape_once(commit.description))) - .commit-row-info - = commit_author_link(commit, avatar: true, size: 24) - authored - .committed_ago - #{time_ago_with_tooltip(commit.committed_date)}   - = link_to_browse_code(project, commit) + .commit-row-info + = commit_author_link(commit, avatar: true, size: 24) + authored + .committed_ago + #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}   + = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 2ee7d73bd2..ce60fbdf03 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -3,9 +3,9 @@ Commits (#{@commits.count}) - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE %ul.well-list - - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE)).each do |commit| + - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit| = render "projects/commits/inline_commit", commit: commit, project: @project %li.warning-row.unstyled other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues. - else - %ul.well-list= render Commit.decorate(@commits), project: @project + %ul.well-list= render Commit.decorate(@commits, @project), project: @project diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index a714f5f79e..e3d8cd0fdd 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,17 +1,22 @@ %ul.nav.nav-tabs = nav_link(controller: [:commit, :commits]) do - = link_to namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do + = icon("history") Commits - %span.badge= number_with_precision(@repository.commit_count, precision: 0, delimiter: ',') + %span.badge= number_with_delimiter(@repository.commit_count) = nav_link(controller: :compare) do - = link_to 'Compare', namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) do + = icon("exchange") + Compare = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do + = icon("code-fork") Branches %span.badge.js-totalbranch-count= @repository.branches.size = nav_link(controller: :tags) do = link_to namespace_project_tags_path(@project.namespace, @project) do + = icon("tags") Tags %span.badge.js-totaltags-count= @repository.tags.length diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 9211de72b1..3854ad5d61 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -1,18 +1,18 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Recent commits to #{@project.name}:#{@ref}" - xml.link :href => namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => namespace_project_commits_url(@project.namespace, @project, @ref), :rel => "alternate", :type => "text/html" + xml.title "#{@project.name}:#{@ref} commits" + xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html" xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any? @commits.each do |commit| xml.entry do - xml.id namespace_project_commit_url(@project.namespace, @project, :id => commit.id) - xml.link :href => namespace_project_commit_url(@project.namespace, @project, :id => commit.id) - xml.title truncate(commit.title, :length => 80) + xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id) + xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) + xml.title truncate(commit.title, length: 80) xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(commit.author_email) + xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(commit.author_email) xml.author do |author| xml.name commit.author_name xml.email commit.author_email diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 7ea855e1a4..55054a3197 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,13 +1,23 @@ +- page_title "Commits", @ref += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") + = render "head" .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' -- if current_user && current_user.private_token - .commits-feed-holder.hidden-xs.hidden-sm - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'btn' do - %i.fa.fa-rss - Commits feed +.commits-feed-holder.hidden-xs.hidden-sm + - if create_mr_button?(@repository.root_ref, @ref) + = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do + = icon('plus') + Create Merge Request + + - if current_user && current_user.private_token + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'prepend-left-10 btn' do + = icon("rss") + %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index dfb1dded9e..3019893d12 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -1,21 +1,22 @@ -= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline' do += form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do .clearfix.append-bottom-20 - if params[:to] && params[:from] = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'} .form-group .input-group.inline-input-group %span.input-group-addon from - = text_field_tag :from, params[:from], class: "form-control" + = text_field_tag :from, params[:from], class: "form-control", required: true = "..." .form-group .input-group.inline-input-group %span.input-group-addon to - = text_field_tag :to, params[:to], class: "form-control" + = text_field_tag :to, params[:to], class: "form-control", required: true   = button_tag "Compare", class: "btn btn-create commits-compare-btn" - - if compare_to_mr_button? - = link_to compare_mr_path, class: 'prepend-left-10 btn' do - %strong Make a merge request + - if create_mr_button? + = link_to create_mr_path, class: 'prepend-left-10 btn' do + = icon("plus") + Create Merge Request :javascript @@ -25,5 +26,3 @@ source: availableTags, minLength: 1 }); - - disableButtonIfEmptyField('#to', '.commits-compare-btn'); diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 4745bfbeaa..d1e579a2ed 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Compare" = render "projects/commits/head" %h3.page-title diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 214b5bd337..3670dd5c13 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,3 +1,4 @@ +- page_title "#{params[:from]}...#{params[:to]}" = render "projects/commits/head" %h3.page-title diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index c577dfa8d5..8d66bae8cd 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -2,24 +2,20 @@ .pull-right - if @available_keys.include?(deploy_key) = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do - %i.fa.fa-plus + = icon('plus') Enable - else - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned? = link_to 'Remove', disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :put, class: "btn btn-remove delete-key btn-sm pull-right" - else = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do - %i.fa.fa-power-off + = icon('power-off') Disable - - if project = project_for_deploy_key(deploy_key) - = link_to namespace_project_deploy_key_path(project.namespace, project, deploy_key) do - %i.fa.fa-key - %strong= deploy_key.title - - else - %i.fa.fa-key - %strong= deploy_key.title - + = icon('key') + %strong= deploy_key.title + %br + %code.key-fingerprint= deploy_key.fingerprint %p.light.prepend-top-10 - if deploy_key.public? diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml index 472a13a852..2e9c5dc08c 100644 --- a/app/views/projects/deploy_keys/index.html.haml +++ b/app/views/projects/deploy_keys/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Deploy Keys" %h3.page-title Deploy keys allow read-only access to the repository diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml index 186d6b5897..01c810aee1 100644 --- a/app/views/projects/deploy_keys/new.html.haml +++ b/app/views/projects/deploy_keys/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Deploy Key" %h3.page-title New Deploy key %hr diff --git a/app/views/projects/deploy_keys/show.html.haml b/app/views/projects/deploy_keys/show.html.haml deleted file mode 100644 index 405b5bcd0d..0000000000 --- a/app/views/projects/deploy_keys/show.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%h3.page-title - Deploy key: - = @key.title - %small - created on - = @key.created_at.stamp("Aug 21, 2011") -.back-link - = link_to namespace_project_deploy_keys_path(@project.namespace, @project) do - ← To keys list -%hr -%pre= @key.key -.pull-right - = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, @key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn-remove btn delete-key" diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index b49aee504f..52c1e03040 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -5,11 +5,13 @@ = parallel_diff_btn = render 'projects/diffs/stats', diffs: diffs -- if show_diff_size_warning?(diffs) - = render 'projects/diffs/warning', diffs: diffs +- diff_files = safe_diff_files(diffs) + +- if diff_files.count < diffs.size + = render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count .files - - safe_diff_files(diffs).each_with_index do |diff_file, index| + - diff_files.each_with_index do |diff_file, index| = render 'projects/diffs/file', diff_file: diff_file, i: index, project: project - if @diff_timeout @@ -18,6 +20,3 @@ Failed to collect changes %p Maybe diff is really big and operation failed with timeout. Try to get diff locally - -:coffeescript - $('.files .diff-header').stick_in_parent(offset_top: $('.navbar').height()) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 672a663532..99ee23a1dd 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -10,8 +10,9 @@ - if @commit.parent_ids.present? = view_file_btn(@commit.parent_id, diff_file, project) - elsif diff_file.diff.submodule? - - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) - = submodule_link(submodule_item, @commit.id) + %span + - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) + = submodule_link(submodule_item, @commit.id, project.repository) - else %span - if diff_file.renamed_file diff --git a/app/views/projects/diffs/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml index 4ebe337973..d1f897b99f 100644 --- a/app/views/projects/diffs/_match_line.html.haml +++ b/app/views/projects/diffs/_match_line.html.haml @@ -1,7 +1,7 @@ -%td.old_line.diff-line-num.unfold.js-unfold{data: {linenumber: line_old}, - class: unfold_bottom_class(bottom)} +%td.old_line.diff-line-num{data: {linenumber: line_old}, + class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} \... -%td.new_line.diff-line-num.unfold.js-unfold{data: {linenumber: line_new}, - class: unfold_bottom_class(bottom)} +%td.new_line.diff-line-num{data: {linenumber: line_new}, + class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} \... %td.line_content.matched= line diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 75f3a80f0d..37fd1b1ec8 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -18,6 +18,8 @@ - elsif type_left == 'old' || type_left.nil? %td.old_line{id: line_code_left, class: "#{type_left}"} = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left + - if @comments_allowed && can?(current_user, :create_note, @project) + = link_to_new_diff_note(line_code_left, 'old') %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw line_content_left - if type_right == 'new' @@ -29,12 +31,14 @@ %td.new_line{id: new_line_code, class: "#{new_line_class}", data: { linenumber: line_number_right }} = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code + - if @comments_allowed && can?(current_user, :create_note, @project) + = link_to_new_diff_note(line_code_right, 'new') %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw line_content_right - if @reply_allowed - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) - if comments_left.present? || comments_right.present? - = render "projects/notes/diff_notes_with_reply_parallel", notes1: comments_left, notes2: comments_right + = render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right - if diff_file.diff.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index e6dfbfd651..977ca423f7 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -1,6 +1,7 @@ - too_big = diff_file.diff_lines.count > Commit::DIFF_SAFE_LINES - if too_big - %a.supp_diff_link Changes suppressed. Click to show + .suppressed-container + %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. %table.text-file{class: "#{'hide' if too_big}"} - last_line = 0 @@ -12,11 +13,11 @@ %tr.line_holder{ id: line_code, class: "#{type}" } - if type == "match" = render "projects/diffs/match_line", {line: line.text, - line_old: line_old, line_new: line.new_pos, bottom: false} + line_old: line_old, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file} - else %td.old_line = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code - - if @comments_allowed && can?(current_user, :write_note, @project) + - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos) , "##{line_code}", id: line_code @@ -29,7 +30,7 @@ - if last_line > 0 = render "projects/diffs/match_line", {line: "", - line_old: last_line, line_new: last_line, bottom: true} + line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file} - if diff_file.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 47abbba2eb..f99bc9a85e 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -1,9 +1,9 @@ .alert.alert-warning %h4 - Too many changes. + Too many changes to show. .pull-right - unless diff_hard_limit_enabled? - = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true)), class: "btn btn-sm btn-warning" + = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm btn-warning" - if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) @@ -14,6 +14,6 @@ = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-sm" %p To preserve performance only - %strong #{allowed_diff_size} of #{diffs.size} + %strong #{shown_files_count} of #{diffs.size} files are displayed. diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index fbf04847e4..e8e65d87f4 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -6,7 +6,7 @@ Project settings %hr .panel-body - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal" }, authenticity_token: true do |f| + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal fieldset-form" }, authenticity_token: true do |f| %fieldset .form-group.project_name_holder @@ -29,7 +29,7 @@ .col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'}) - = render "visibility_level", f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can?(current_user, :change_visibility_level, @project) + = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project .form-group = f.label :tag_list, "Tags", class: 'control-label' @@ -41,39 +41,46 @@ %legend Features: .form-group - = f.label :issues_enabled, "Issues", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :issues_enabled - %span.descr Lightweight issue tracking system for this project + = f.label :issues_enabled do + = f.check_box :issues_enabled + %strong Issues + %br + %span.descr Lightweight issue tracking system for this project .form-group - = f.label :merge_requests_enabled, "Merge Requests", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :merge_requests_enabled - %span.descr Submit changes to be merged upstream. + = f.label :merge_requests_enabled do + = f.check_box :merge_requests_enabled + %strong Merge Requests + %br + %span.descr Submit changes to be merged upstream. .form-group - = f.label :wiki_enabled, "Wiki", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :wiki_enabled - %span.descr Pages for project documentation + = f.label :wiki_enabled do + = f.check_box :wiki_enabled + %strong Wiki + %br + %span.descr Pages for project documentation .form-group - = f.label :snippets_enabled, "Snippets", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :snippets_enabled - %span.descr Share code pastes with others out of git repository + = f.label :snippets_enabled do + = f.check_box :snippets_enabled + %strong Snippets + %br + %span.descr Share code pastes with others out of git repository %fieldset.features %legend Project avatar: .form-group - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 - if @project.avatar? = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') %p.light @@ -169,7 +176,7 @@ .form-group = label_tag :new_namespace_id, nil, class: 'control-label' do %span Namespace - .col-sm-10 + .col-sm-9 .form-group = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' } %ul diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 49806ceaa9..798f1c47da 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -4,44 +4,47 @@ = render "home_panel" -.center.well - %h3 +.center.light-well + %h3.page-title The repository for this project is empty - %h4 - You can - = link_to namespace_project_new_blob_path(@project.namespace, @project, 'master'), class: 'btn btn-new btn-lg' do - add a file -  or do a push via the command line. + %p + If you already have files you can push them using command line instructions below. + %br + Otherwise you can start with + = link_to "adding README", new_readme_path, class: 'underlined-link' + file to this project. -%h4 - %strong Command line instructions +.prepend-top-20 +%h3.page-title + Command line instructions %div.git-empty %fieldset - %legend Git global setup - %pre.dark + %h5 Git global setup + %pre.light-well :preserve - git config --global user.name "#{git_user_name}" - git config --global user.email "#{git_user_email}" + git config --global user.name "#{h git_user_name}" + git config --global user.email "#{h git_user_email}" %fieldset - %legend Create a new repository - %pre.dark + %h5 Create a new repository + %pre.light-well :preserve - mkdir #{@project.path} - cd #{@project.path} - git init + git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + cd #{h @project.path} touch README.md git add README.md - git commit -m "first commit" - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git commit -m "add README" git push -u origin master %fieldset - %legend Push an existing Git repository - %pre.dark + %h5 Existing folder or Git repository + %pre.light-well :preserve - cd existing_git_repo + cd existing_folder + git init git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git add . + git commit git push -u origin master - if can? current_user, :remove_project, @project diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml index 8eb4f79597..3d0ab5b85d 100644 --- a/app/views/projects/forks/error.html.haml +++ b/app/views/projects/forks/error.html.haml @@ -1,3 +1,4 @@ +- page_title "Fork project" - if @forked_project && !@forked_project.saved? .alert.alert-danger.alert-block %h4 diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 5a6c46f320..b7a2ed68e2 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -1,3 +1,4 @@ +- page_title "Fork project" %h3.page-title Fork project %p.lead Click to fork the project to a user or group diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 78b4c1923d..141acbdcf7 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,8 +1,11 @@ +- page_title "Commit statistics" +.tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs_commits' = render 'head' %p.lead Commit statistics for - %strong #{@repository.root_ref} + %strong #{@ref} #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} .row diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index e3d5094ddc..ecdd0eaf52 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,4 +1,8 @@ +- page_title "Contributor statistics" +.tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs' = render 'head' + .loading-graph .center %h3.page-title @@ -10,7 +14,7 @@ .header.clearfix %h3#date_header.page-title %p.light - Commits to #{@project.default_branch}, excluding merge commits. Limited by 6,000 commits + Commits to #{@ref}, excluding merge commits. Limited by 6,000 commits %input#brush_change{:type => "hidden"} .graphs #contributors-master @@ -34,4 +38,3 @@ $(".stat-graph").fadeIn(); $(".loading-graph").hide(); dataType: "json" - diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index bbaddba31b..eadbf61fdd 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Web Hooks" %h3.page-title Web hooks @@ -33,6 +34,13 @@ %strong Tag push events %p.light This url will be triggered when a new tag is pushed to the repository + %div + = f.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = f.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment %div = f.check_box :issues_events, class: 'pull-left' .prepend-left-20 @@ -63,6 +71,6 @@ .clearfix %span.monospace= hook.url %p - - %w(push_events tag_push_events issues_events merge_requests_events).each do |trigger| + - %w(push_events tag_push_events issues_events note_events merge_requests_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 934b6b8c01..f8f2e192e2 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -1,3 +1,4 @@ +- page_title "Import repository" %h3.page-title - if @project.import_failed? Import failed. Retry? diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 2d1fdafed2..39fe0fc1c4 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Import in progress" .save-project-loader .center %h2 diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 288b48f458..f61ae95720 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,33 +1,33 @@ - content_for :note_actions do - - if can?(current_user, :modify_issue, @issue) + - if can?(current_user, :update_issue, @issue) - if @issue.closed? - = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue' + = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue' - else - = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" + = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue' + += render 'shared/show_aside' + .row %section.col-md-9 .votes-holder.pull-right #votes= render 'votes/votes_block', votable: @issue .participants - %span= pluralize(@issue.participants(current_user).count, 'participant') - - @issue.participants(current_user).each do |participant| + %span= pluralize(@participants.count, 'participant') + - @participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) - .voting_notes#notes= render "projects/notes/notes_with_form" + .voting_notes#notes= render 'projects/notes/notes_with_form' %aside.col-md-3 .issuable-affix .clearfix - %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} + %span.slead.has_tooltip{title: 'Cross-project reference'} = cross_project_reference(@project, @issue) %hr .context - = render partial: 'issue_context', locals: { issue: @issue } + = render 'shared/issuable/context', issuable: @issue - if @issue.labels.any? .issuable-context-title %label Labels .issue-show-labels - @issue.labels.each do |label| - = link_to namespace_project_issues_path(@project.namespace, @project, label_name: label.name) do - = render_colored_label(label) - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left + = link_to_label(label) diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 7d7217eb2a..f39bb7d257 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -3,12 +3,10 @@ %hr = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| - = render 'projects/issuable_form', f: f, issuable: @issue + = render 'shared/issuable/form', f: f, issuable: @issue :javascript $('.assign-to-me-link').on('click', function(e){ $('#issue_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); - - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 998e74d12c..b6910c8f79 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,29 +1,38 @@ %li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) } - - if controller.controller_name == 'issues' + - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .issue-check - = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue", disabled: !can?(current_user, :modify_issue, issue) + = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" .issue-title - %span.str-truncated + %span.issue-title-text = link_to_gfm issue.title, issue_path(issue), class: "row_title" + .issue-labels + - issue.labels.each do |label| + = link_to_label(label, project: issue.project) .pull-right.light - if issue.closed? %span CLOSED + - if issue.assignee + = link_to_member(@project, issue.assignee, name: false) - note_count = issue.notes.user.count - if note_count > 0   %span %i.fa.fa-comments = note_count + - else +   + %span.issue-no-comments + %i.fa.fa-comments + = 0 .issue-info - = link_to "##{issue.iid}", issue_path(issue), class: "light" - - if issue.assignee - assigned to #{link_to_member(@project, issue.assignee)} + = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe - if issue.votes_count > 0 = render 'votes/votes_inline', votable: issue - if issue.milestone +   %span %i.fa.fa-clock-o = issue.milestone.title @@ -32,21 +41,4 @@ = issue.task_status .pull-right.issue-updated-at - %small updated #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_update_ago')} - - .issue-labels - - issue.labels.each do |label| - = link_to namespace_project_issues_path(issue.project.namespace, issue.project, label_name: label.name) do - = render_colored_label(label) - - .issue-actions - - if can? current_user, :modify_issue, issue - - if issue.closed? - = link_to 'Reopen', issue_path(issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-sm btn-grouped reopen_issue btn-reopen", remote: true - - else - = link_to 'Close', issue_path(issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-sm btn-grouped close_issue btn-close", remote: true - = link_to edit_namespace_project_issue_path(issue.project.namespace, issue.project, issue), class: "btn btn-sm edit-issue-link btn-grouped" do - %i.fa.fa-pencil-square-o - Edit - - + %small updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')} diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml deleted file mode 100644 index 9228074d83..0000000000 --- a/app/views/projects/issues/_issue_context.html.haml +++ /dev/null @@ -1,46 +0,0 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f| - %div.prepend-top-20 - .issuable-context-title - %label - Assignee: - - if issue.assignee - %strong= link_to_member(@project, @issue.assignee, size: 24) - - else - none - - if can?(current_user, :modify_issue, @issue) - = users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id, null_user: true, first_user: true) - - %div.prepend-top-20.clearfix - .issuable-context-title - %label - Milestone: - - if issue.milestone - %span.back-to-milestone - = link_to namespace_project_milestone_path(@project.namespace, @project, @issue.milestone) do - %strong - %i.fa.fa-clock-o - = @issue.milestone.title - - else - none - - if can?(current_user, :modify_issue, @issue) - = f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) - = hidden_field_tag :issue_context - = f.submit class: 'btn' - - - if current_user - %div.prepend-top-20.clearfix - .issuable-context-title - %label - Subscription: - %button.btn.btn-block.subscribe-button - %i.fa.fa-eye - %span= @issue.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" - - subscribtion_status = @issue.subscribed?(current_user) ? "subscribed" : "unsubscribed" - .subscription-status{"data-status" => subscribtion_status} - .description-block.unsubscribed{class: ( "hidden" if @issue.subscribed?(current_user) )} - You're not receiving notifications from this thread. - .description-block.subscribed{class: ( "hidden" unless @issue.subscribed?(current_user) )} - You're receiving notifications because you're subscribed to this thread. - -:coffeescript - new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index b1bc3ba0eb..53b6f0879c 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1 +1,2 @@ +- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues" = render "form" diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 126f2c07fa..dc8e477185 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -1,8 +1,8 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name} issues" - xml.link :href => namespace_project_issues_url(@project.namespace, @project, :atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => namespace_project_issues_url(@project.namespace, @project), :rel => "alternate", :type => "text/html" + xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_issues_url(@project.namespace, @project) xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index d3c7ae24a7..d06225f548 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,3 +1,8 @@ +- page_title "Issues" += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") + .append-bottom-10 .pull-right .pull-left @@ -6,14 +11,14 @@ = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do %i.fa.fa-rss - = render 'shared/issuable_search_form', path: namespace_project_issues_path(@project.namespace, @project) + = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - - if can? current_user, :write_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do + - if can? current_user, :create_issue, @project + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do %i.fa.fa-plus New Issue - = render 'shared/issuable_filter' + = render 'shared/issuable/filter', type: :issues .issues-holder = render "issues" diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml index b1bc3ba0eb..da6edd5c2d 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1 +1,2 @@ +- page_title "New Issue" = render "form" diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bd28d8a1db..e7b14e7582 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,5 +1,6 @@ +- page_title "#{@issue.title} (##{@issue.iid})", "Issues" .issue - .issue-details + .issue-details.issuable-details %h4.page-title .issue-box{ class: issue_box_class(@issue) } - if @issue.closed? @@ -8,21 +9,27 @@ Open Issue ##{@issue.iid} %small.creator - · created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)} + · created by #{link_to_member(@project, @issue.author)} + = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') + - if @issue.updated_at != @issue.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') .pull-right - - if can?(current_user, :write_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-grouped new-issue-link", title: "New Issue", id: "new_issue_link" do - %i.fa.fa-plus + - if can?(current_user, :create_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do + = icon('plus') New Issue - - if can?(current_user, :modify_issue, @issue) + - if can?(current_user, :update_issue, @issue) - if @issue.closed? - = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" + = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen' - else - = link_to 'Close', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" + = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue' - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: "btn btn-grouped issuable-edit" do - %i.fa.fa-pencil-square-o + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do + = icon('pencil-square-o') Edit %hr @@ -30,11 +37,13 @@ = gfm escape_once(@issue.title) %div - if @issue.description.present? - .description + .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@issue.description, parse_tasks: true) + = markdown(@issue.description) + %textarea.hidden.js-task-list-field + = @issue.description %hr .issue-discussion - = render "projects/issues/discussion" + = render 'projects/issues/discussion' diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 1d38662bff..b7735aaf3c 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -1,17 +1,3 @@ -- if params[:status_only] - - if @issue.valid? - :plain - $("##{dom_id(@issue)}").fadeOut(); -- elsif params[:issue_context] - $('.context').html("#{escape_javascript(render partial: 'issue_context', locals: { issue: @issue })}"); - $('.context').effect('highlight'); - - if @issue.milestone - $('.milestone-nav-link').replaceWith("| Milestone #{escape_javascript(link_to @issue.milestone.title, namespace_project_milestone_path(@issue.project.namespace, @issue.project, @issue.milestone))}") - - else - $('.milestone-nav-link').html('') - - -$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) -$('.edit-issue.inline-update input[type="submit"]').hide(); -new UsersSelect() +$('.context').html("#{escape_javascript(render 'shared/issuable/context', issuable: @issue)}"); +$('.context').effect('highlight') new Issue(); diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 261d52dedc..534c545329 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -1,7 +1,7 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f| -if @label.errors.any? .row - .col-sm-10.col-sm-offset-2 + .col-sm-offset-2.col-sm-10 .alert.alert-danger - @label.errors.full_messages.each do |msg| %span= msg diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 8282945286..c6ebfa281a 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,10 +1,10 @@ %li{id: dom_id(label)} - = render_colored_label(label) + = link_to_label(label) .pull-right %strong.append-right-20 - = link_to namespace_project_issues_path(@project.namespace, @project, label_name: label.name) do + = link_to_label(label) do = pluralize label.open_issues_count, 'open issue' - if can? current_user, :admin_label, @project - = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn' - = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm' + = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index e003d1dfe7..645402667f 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @label.name, "Labels" %h3 Edit label %span.light #{@label.name} diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 0700e72d39..d44fe48621 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Labels" - if can? current_user, :admin_label, @project = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do New label @@ -12,4 +13,7 @@ = paginate @labels, theme: 'gitlab' - else .light-well - .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + - if can? current_user, :admin_label, @project + .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + - else + .nothing-here-block No labels created diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 0683ed5d4f..b3ef17025c 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Label" %h3 New label .back-link = link_to namespace_project_labels_path(@project.namespace, @project) do diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index eb72eaabd8..f855dfec32 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,10 +1,12 @@ - content_for :note_actions do - - if can?(current_user, :modify_merge_request, @merge_request) + - if can?(current_user, :update_merge_request, @merge_request) - if @merge_request.open? = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - if @merge_request.closed? = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" += render 'shared/show_aside' + .row %section.col-md-9 .votes-holder.pull-right @@ -18,14 +20,11 @@ = cross_project_reference(@project, @merge_request) %hr .context - = render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request } + = render 'shared/issuable/context', issuable: @merge_request - if @merge_request.labels.any? .issuable-context-title %label Labels .merge-request-show-labels - @merge_request.labels.each do |label| - = link_to namespace_project_merge_requests_path(@project.namespace, @project, label_name: label.name) do - = render_colored_label(label) - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left + = link_to_label(label) diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 1c7160bce5..9cf389dbe3 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,12 +1,9 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| .merge-request-form-info - = render 'projects/issuable_form', f: f, issuable: @merge_request + = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript - disableButtonIfEmptyField("#merge_request_title", ".btn-save"); $('.assign-to-me-link').on('click', function(e){ $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); - - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 4f30d1e69f..0bcd543fee 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,7 +1,10 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title - %span.str-truncated + %span.merge-request-title-text = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" + .merge-request-labels + - merge_request.labels.each do |label| + = link_to_label(label, project: merge_request.project) .pull-right.light - if merge_request.merged? %span @@ -9,7 +12,7 @@ MERGED - elsif merge_request.closed? %span - %i.fa.fa-close + %i.fa.fa-ban CLOSED - else %span.hidden-xs.hidden-sm @@ -17,20 +20,26 @@ %i.fa.fa-code-fork %span= merge_request.target_branch - note_count = merge_request.mr_and_commit_notes.user.count + - if merge_request.assignee +   + = link_to_member(merge_request.source_project, merge_request.assignee, name: false) - if note_count > 0   %span %i.fa.fa-comments = note_count + - else +   + %span.merge-request-no-comments + %i.fa.fa-comments + = 0 + .merge-request-info - = link_to "##{merge_request.iid}", merge_request_path(merge_request), class: "light" - - if merge_request.assignee - assigned to #{link_to_member(merge_request.source_project, merge_request.assignee)} - - else - Unassigned + = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe - if merge_request.votes_count > 0 = render 'votes/votes_inline', votable: merge_request - if merge_request.milestone_id? +   %span %i.fa.fa-clock-o = merge_request.milestone.title @@ -38,11 +47,5 @@ %span.task-status = merge_request.task_status - .pull-right.hidden-xs - %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')} - - .merge-request-labels - - merge_request.labels.each do |label| - = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, label_name: label.name) do - = render_colored_label(label) + %small updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')} diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 17e76059fd..ff9c0cdb28 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -1,7 +1,6 @@ -%h3.page-title Compare branches for new Merge Request -%hr +%p.lead Compare branches for new Merge Request -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors .merge-request-branches.row .col-md-6 @@ -9,9 +8,9 @@ .panel-heading %strong Source branch .panel-body - = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted? }) + = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted?, required: true })   - = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2'}) + = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2', required: true}) .panel-footer .mr_source_commit @@ -21,9 +20,9 @@ %strong Target branch .panel-body - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] - = f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted? }) + = f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted?, required: true })   - = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2'}) + = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2', required: true}) .panel-footer .mr_target_commit @@ -52,8 +51,8 @@ are the same. - %hr - = f.submit 'Compare branches', class: "btn btn-primary mr-compare-btn" + %div + = f.submit 'Compare branches', class: "btn btn-new mr-compare-btn" :javascript var source_branch = $("#merge_request_source_branch") diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 4e72458932..633a54f362 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -7,105 +7,43 @@ %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} %span.pull-right - = link_to 'Change branches', new_namespace_project_merge_request_path(@project.namespace, @project) - -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: "merge-request-form form-horizontal gfm-form" } do |f| + = link_to 'Change branches', mr_change_branches_path(@merge_request) +%hr += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| .merge-request-form-info - .form-group - = f.label :title, class: 'control-label' do - %strong Title * - .col-sm-10 - = f.text_field :title, maxlength: 255, autofocus: true, class: 'form-control pad js-gfm-input', required: true - .form-group.issuable-description - = f.label :description, 'Description', class: 'control-label' - .col-sm-10 - = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' - - .col-sm-12-hint - .pull-left - Parsed with - #{link_to 'Gitlab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. - .pull-right - Attach files by dragging & dropping - or #{link_to 'selecting them', '#', class: 'markdown-selector'}. - - .clearfix - .error-alert - %hr - .form-group - .issue-assignee - = f.label :assignee_id, class: 'control-label' do - %i.fa.fa-user - Assign to - .col-sm-10 - = users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id) -   - = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' - .form-group - .issue-milestone - = f.label :milestone_id, class: 'control-label' do - %i.fa.fa-clock-o - Milestone - .col-sm-10 - - if milestone_options(@merge_request).present? - = f.select(:milestone_id, milestone_options(@merge_request), {include_blank: 'Select milestone'}, {class: 'select2'}) - - else - %span.light No open milestones available. -   - - if can? current_user, :admin_milestone, @merge_request.target_project - = link_to 'Create new milestone', new_namespace_project_milestone_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank - .form-group - = f.label :label_ids, class: 'control-label' do - %i.fa.fa-tag - Labels - .col-sm-10 - - if @merge_request.target_project.labels.any? - = f.collection_select :label_ids, @merge_request.target_project.labels.all, :id, :name, {selected: @merge_request.label_ids}, multiple: true, class: 'select2' - - else - %span.light No labels yet. -   - - if can? current_user, :admin_label, @merge_request.target_project - = link_to 'Create new label', new_namespace_project_label_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank - - .form-actions - - if guide_url = contribution_guide_url(@target_project) - %p - Please review the - %strong #{link_to 'guidelines for contribution', guide_url} - to this repository. - = f.hidden_field :source_project_id - = f.hidden_field :source_branch - = f.hidden_field :target_project_id - = f.hidden_field :target_branch - = f.submit 'Submit merge request', class: 'btn btn-create' + = render 'shared/issuable/form', f: f, issuable: @merge_request + = f.hidden_field :source_project_id + = f.hidden_field :source_branch + = f.hidden_field :target_project_id + = f.hidden_field :target_branch .mr-compare.merge-request %ul.nav.nav-tabs.merge-request-tabs - %li.commits-tab{data: {action: 'commits', toggle: 'tab'}} - = link_to url_for(params) do - %i.fa.fa-history + %li.commits-tab + = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = icon('history') Commits %span.badge= @commits.size - %li.diffs-tab{data: {action: 'diffs', toggle: 'tab'}} - = link_to url_for(params) do - %i.fa.fa-list-alt + %li.diffs-tab + = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = icon('list-alt') Changes %span.badge= @diffs.size - .commits.tab-content - = render "projects/commits/commits", project: @project - .diffs.tab-content - - if @diffs.present? - = render "projects/diffs/diffs", diffs: @diffs, project: @project - - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - .alert.alert-danger - %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. - %p To preserve performance the line changes are not shown. - - else - .alert.alert-danger - %h4 This comparison includes a huge diff. - %p To preserve performance the line changes are not shown. + .tab-content + #commits.commits.tab-pane + = render "projects/commits/commits", project: @project + #diffs.diffs.tab-pane + - if @diffs.present? + = render "projects/diffs/diffs", diffs: @diffs, project: @project + - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + .alert.alert-danger + %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. + %p To preserve performance the line changes are not shown. + - else + .alert.alert-danger + %h4 This comparison includes a huge diff. + %p To preserve performance the line changes are not shown. :javascript $('.assign-to-me-link').on('click', function(e){ @@ -113,11 +51,11 @@ e.preventDefault(); }); - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; - :javascript var merge_request merge_request = new MergeRequest({ - action: 'commits' + action: 'new', + diffs_loaded: true, + commits_loaded: true }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index a74aede4e6..007f6c6a78 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,77 +1,72 @@ +- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" .merge-request{'data-url' => merge_request_path(@merge_request)} - .merge-request-details + .merge-request-details.issuable-details = render "projects/merge_requests/show/mr_title" %hr = render "projects/merge_requests/show/mr_box" %hr - .append-bottom-20 - .slead - %span From - - if @merge_request.for_fork? - %strong.label-branch< - - if @merge_request.source_project - = link_to @merge_request.source_project_namespace, namespace_project_path(@merge_request.source_project.namespace, @merge_request.source_project) - - else - \ #{@merge_request.source_project_namespace} - \:#{@merge_request.source_branch} - %span into - %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} - - else - %strong.label-branch #{@merge_request.source_branch} - %span into - %strong.label-branch #{@merge_request.target_branch} - - if @merge_request.open? - %span.pull-right - .btn-group - %a.btn.dropdown-toggle{ data: {toggle: :dropdown} } - %i.fa.fa-download - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) - %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) + .append-bottom-20.mr-source-target + - if @merge_request.open? + .pull-right + - if @merge_request.source_branch_exists? + = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do + = icon('cloud-download fw') + Check out branch + + %span.dropdown + %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } + = icon('download') + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) + %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) + .light + %span Request to merge + %span.label-branch #{source_branch_with_namespace(@merge_request)} + %span into + %span.label-branch #{@merge_request.target_branch} = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/show/state_widget" + = render "projects/merge_requests/widget/show.html.haml" + + - if @merge_request.open? && @merge_request.can_be_merged? + .light + You can also accept this merge request manually using the + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs - %li.notes-tab{data: {action: 'notes'}} - = link_to merge_request_path(@merge_request) do - %i.fa.fa-comments + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do + = icon('comments') Discussion %span.badge= @merge_request.mr_and_commit_notes.user.count - %li.commits-tab{data: {action: 'commits'}} - = link_to merge_request_path(@merge_request), title: 'Commits' do - %i.fa.fa-history + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = icon('history') Commits %span.badge= @commits.size - %li.diffs-tab{data: {action: 'diffs'}} - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) do - %i.fa.fa-list-alt + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = icon('list-alt') Changes %span.badge= @merge_request.diffs.size - .notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" } - = render "projects/merge_requests/discussion" - .commits.tab-content - = render "projects/merge_requests/show/commits" - .diffs.tab-content - - if current_page?(action: 'diffs') - = render "projects/merge_requests/show/diffs" + .tab-content + #notes.notes.tab-pane.voting_notes + = render "projects/merge_requests/discussion" + #commits.commits.tab-pane + - # This tab is always loaded via AJAX + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX .mr-loading-status = spinner - :javascript var merge_request; merge_request = new MergeRequest({ - url_to_automerge_check: "#{automerge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", - check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", - ci_enable: #{@project.ci_service ? "true" : "false"}, - current_status: "#{@merge_request.merge_status_name}", action: "#{controller.action_name}" }); diff --git a/app/views/projects/merge_requests/automerge.js.haml b/app/views/projects/merge_requests/automerge.js.haml index a53cbb150a..33321651e3 100644 --- a/app/views/projects/merge_requests/automerge.js.haml +++ b/app/views/projects/merge_requests/automerge.js.haml @@ -1,6 +1,6 @@ --if @status +- if @status :plain - merge_request.mergeInProgress(); --else + merge_request_widget.mergeInProgress(); +- else :plain - merge_request.alreadyOrCannotBeMerged() + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/branch_from.js.haml b/app/views/projects/merge_requests/branch_from.js.haml index 8372afa61b..9210798f39 100644 --- a/app/views/projects/merge_requests/branch_from.js.haml +++ b/app/views/projects/merge_requests/branch_from.js.haml @@ -1,2 +1,3 @@ :plain $(".mr_source_commit").html("#{commit_to_html(@commit, @source_project, false)}"); + $('.js-timeago').timeago() diff --git a/app/views/projects/merge_requests/branch_to.js.haml b/app/views/projects/merge_requests/branch_to.js.haml index f7ede0ded5..32fe2d535f 100644 --- a/app/views/projects/merge_requests/branch_to.js.haml +++ b/app/views/projects/merge_requests/branch_to.js.haml @@ -1,2 +1,3 @@ :plain $(".mr_target_commit").html("#{commit_to_html(@commit, @target_project, false)}"); + $('.js-timeago').timeago() diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 839c63986a..7e5cb07f24 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" %h3.page-title = "Edit merge request ##{@merge_request.iid}" %hr diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index d7992bdd19..72fbe2e27a 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,11 +1,14 @@ +- page_title "Merge Requests" += render 'projects/last_push' .append-bottom-10 .pull-right - = render 'shared/issuable_search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) + = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) - - if can? current_user, :write_merge_request, @project - = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request - = render 'shared/issuable_filter' + - if can? current_user, :create_merge_request, @project + .pull-left.hidden-xs + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do + %i.fa.fa-plus + New Merge Request + = render 'shared/issuable/filter', type: :merge_requests .merge-requests-holder = render 'merge_requests' diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index b9c466657d..15bd4e2faf 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,3 +1,4 @@ +- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" .merge-request = render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_box" diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml index 4756903d0e..b038a640f6 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Merge Request" - if @merge_request.can_be_created = render 'new_submit' - else diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml deleted file mode 100644 index 105562fb05..0000000000 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f| - %div.prepend-top-20 - .issuable-context-title - %label - Assignee: - - if @merge_request.assignee - %strong= link_to_member(@project, @merge_request.assignee, size: 24) - - else - none - .issuable-context-selectbox - - if can?(current_user, :modify_merge_request, @merge_request) - = users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id, null_user: true) - - %div.prepend-top-20.clearfix - .issuable-context-title - %label - Milestone: - - if @merge_request.milestone - %span.back-to-milestone - = link_to namespace_project_milestone_path(@project.namespace, @project, @merge_request.milestone) do - %strong - %i.fa.fa-clock-o - = @merge_request.milestone.title - - else - none - .issuable-context-selectbox - - if can?(current_user, :modify_merge_request, @merge_request) - = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) - = hidden_field_tag :merge_request_context - = f.submit class: 'btn' - - - if current_user - %div.prepend-top-20.clearfix - .issuable-context-title - %label - Subscription: - %button.btn.btn-block.subscribe-button - %i.fa.fa-eye - %span= @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" - - subscribtion_status = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed" - .subscription-status{"data-status" => subscribtion_status} - .description-block.unsubscribed{class: ( "hidden" if @merge_request.subscribed?(current_user) )} - You're not receiving notifications from this thread. - .description-block.subscribed{class: ( "hidden" unless @merge_request.subscribed?(current_user) )} - You're receiving notifications because you're subscribed to this thread. - -:coffeescript - new Subscription("#{toggle_subscription_namespace_project_merge_request_path(@merge_request.project.namespace, @project, @merge_request)}") diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 63db4b3096..f18cf96c17 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -1,45 +1,47 @@ -%div#modal_merge_info.modal.hide +%div#modal_merge_info.modal .modal-dialog .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3 How to merge + %h3 Check out, review and merge locally .modal-body - - if @merge_request.for_fork? - - source_remote = @merge_request.source_project.namespace.nil? ? "source" :@merge_request.source_project.namespace.path - - target_remote = @merge_request.target_project.namespace.nil? ? "target" :@merge_request.target_project.namespace.path - %p - %strong Step 1. - Fetch the code and create a new branch pointing to it - %pre.dark + %p + %strong Step 1. + Fetch and check out the branch for this merge request + %pre.dark + - if @merge_request.for_fork? :preserve - git fetch #{@merge_request.source_project.http_url_to_repo} #{@merge_request.source_branch} - git checkout -b #{@merge_request.source_project_path}-#{@merge_request.source_branch} FETCH_HEAD - %p - %strong Step 2. - Merge the branch and push the changes to GitLab - %pre.dark - :preserve - git checkout #{@merge_request.target_branch} - git merge --no-ff #{@merge_request.source_project_path}-#{@merge_request.source_branch} - git push origin #{@merge_request.target_branch} - - else - %p - %strong Step 1. - Update the repo and checkout the branch we are going to merge - %pre.dark + git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch} + git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD + - else :preserve git fetch origin - git checkout -b #{@merge_request.source_branch} origin/#{@merge_request.source_branch} - %p - %strong Step 2. - Merge the branch and push the changes to GitLab - %pre.dark - :preserve - git checkout #{@merge_request.target_branch} - git merge --no-ff #{@merge_request.source_branch} - git push origin #{@merge_request.target_branch} + git checkout -b #{h @merge_request.source_branch} origin/#{h @merge_request.source_branch} + %p + %strong Step 2. + Review the changes locally + %p + %strong Step 3. + Merge the branch and fix any conflicts that come up + %pre.dark + - if @merge_request.for_fork? + :preserve + git checkout #{h @merge_request.target_branch} + git merge --no-ff #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} + - else + :preserve + git checkout #{h @merge_request.target_branch} + git merge --no-ff #{h @merge_request.source_branch} + %p + %strong Step 4. + Push the result of the merge to GitLab + %pre.dark + :preserve + git push origin #{h @merge_request.target_branch} + - unless @merge_request.can_be_merged_by?(current_user) + %p + Note that pushing to GitLab requires write access to this repository. :javascript $(function(){ diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml deleted file mode 100644 index 9f51f84d40..0000000000 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ /dev/null @@ -1,74 +0,0 @@ -- unless @allowed_to_merge - - if @project.archived? - %p - %strong Archived projects cannot be committed to! - - else - .automerge_widget.cannot_be_merged.hide - %strong This can't be merged automatically, even if it could be merged you don't have the permission to do so. - .automerge_widget.can_be_merged.hide - %strong This can be merged automatically but you don't have the permission to do so. - - -- if @show_merge_controls - .automerge_widget.can_be_merged.hide - .clearfix - = form_for [:automerge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post do |f| - .accept-merge-holder.clearfix.js-toggle-container - .accept-action - = f.submit "Accept Merge Request", class: "btn btn-create accept_merge_request" - - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? - .accept-control.checkbox - = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do - = check_box_tag :should_remove_source_branch - Remove source-branch - .accept-control - = link_to "#", class: "modify-merge-commit-link js-toggle-button", title: "Modify merge commit message" do - %i.fa.fa-edit - Modify commit message - .js-toggle-content.hide.prepend-top-20 - = render 'shared/commit_message_container', params: params, - text: @merge_request.merge_commit_message, - rows: 14, hint: true - - %br - .light - If you still want to merge this request manually - use - %strong - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - - - .automerge_widget.no_satellite.hide - %p - %span - %strong This repository does not have satellite. Ask an administrator to fix this issue - - .automerge_widget.cannot_be_merged.hide - %h4 - This request can't be merged with GitLab. - You should do it manually with - %strong - = link_to "#modal_merge_info", class: "underlined-link how_to_merge_link", title: "How To Merge", "data-toggle" => "modal" do - command line - - %p - %button.btn.disabled - %i.fa.fa-warning - Accept Merge Request -   - This usually happens when git can not resolve conflicts between branches automatically. - - .automerge_widget.unchecked - %p - %strong - %i.fa.fa-spinner.fa-spin - Checking for ability to automatically merge… - - .automerge_widget.already_cannot_be_merged.hide - %p - %strong This merge request can not be merged. Try to reload the page. - - .merge-in-progress.hide - %p - %i.fa.fa-spinner.fa-spin -   - Merge is in progress. Please wait. Page will be automatically reloaded.   diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index ada9ae58b8..e3cd434687 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -3,7 +3,9 @@ %div - if @merge_request.description.present? - .description + .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description, parse_tasks: true) + = markdown(@merge_request.description) + %textarea.hidden.js-task-list-field + = @merge_request.description diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml deleted file mode 100644 index ffa3f7b0e3..0000000000 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -- if @commits.any? - .ci_widget.ci-success{style: "display:none"} - %i.fa.fa-check - %span CI build passed - for #{@merge_request.last_commit_short_sha}. - = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - - - .ci_widget.ci-failed{style: "display:none"} - %i.fa.fa-times - %span CI build failed - for #{@merge_request.last_commit_short_sha}. - = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - - - [:running, :pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} - %i.fa.fa-clock-o - %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. - = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - - .ci_widget - %i.fa.fa-spinner - Checking for CI status for #{@merge_request.last_commit_short_sha} - - .ci_widget.ci-canceled{style: "display:none"} - %i.fa.fa-times - %span CI build canceled - for #{@merge_request.last_commit_short_sha}. - = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - - .ci_widget.ci-error{style: "display:none"} - %i.fa.fa-times - %span Cannot connect to the CI server. Please check your settings and try again. diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 46e92a9c55..9a1eb36fc8 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,18 +1,19 @@ %h4.page-title .issue-box{ class: issue_box_class(@merge_request) } - - if @merge_request.merged? - Merged - - elsif @merge_request.closed? - Closed - - else - Open - = "Merge Request ##{@merge_request.iid}" + = @merge_request.state_human_name + Merge Request ##{@merge_request.iid} %small.creator · - created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)} + created by #{link_to_member(@project, @merge_request.author)} + = time_ago_with_tooltip(@merge_request.created_at) + - if @merge_request.updated_at != @merge_request.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom') .issue-btn-group.pull-right - - if can?(current_user, :modify_merge_request, @merge_request) + - if can?(current_user, :update_merge_request, @merge_request) - if @merge_request.open? = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request" = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do diff --git a/app/views/projects/merge_requests/show/_no_accept.html.haml b/app/views/projects/merge_requests/show/_no_accept.html.haml deleted file mode 100644 index 423fcd48e2..0000000000 --- a/app/views/projects/merge_requests/show/_no_accept.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%h4 - Can't be merged -%p - This merge request can not be accepted because branch - - unless @merge_request.source_branch_exists? - %span.label.label-inverse= @merge_request.source_branch - does not exist in - %span.label.label-info= @merge_request.source_project_path - - else - %span.label.label-inverse= @merge_request.target_branch - does not exist in - %span.label.label-info= @merge_request.target_project_path - %br - %strong Please close this merge request or change branches with existing one diff --git a/app/views/projects/merge_requests/show/_participants.html.haml b/app/views/projects/merge_requests/show/_participants.html.haml index 9c93fa55fe..c67afe963e 100644 --- a/app/views/projects/merge_requests/show/_participants.html.haml +++ b/app/views/projects/merge_requests/show/_participants.html.haml @@ -1,4 +1,4 @@ .participants - %span #{@merge_request.participants(current_user).count} participants - - @merge_request.participants(current_user).each do |participant| + %span #{@participants.count} participants + - @participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) diff --git a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml deleted file mode 100644 index 59cb85edfc..0000000000 --- a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- if @source_branch.blank? - Source branch has been removed - -- elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && @merge_request.merged? - .remove_source_branch_widget - %p Changes merged into #{@merge_request.target_branch}. You can remove source branch now - = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do - %i.fa.fa-times - Remove Source Branch - - .remove_source_branch_widget.failed.hide - Failed to remove source branch '#{@merge_request.source_branch}' - - .remove_source_branch_in_progress.hide - %i.fa.fa-spinner.fa-spin -   - Removing source branch '#{@merge_request.source_branch}'. Please wait. Page will be automatically reloaded.   diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml deleted file mode 100644 index 44bd9347f5..0000000000 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -.mr-state-widget - - if @merge_request.source_project.ci_service && @commits.any? - .mr-widget-heading - = render "projects/merge_requests/show/mr_ci" - .mr-widget-body - - if @merge_request.open? - - if @merge_request.source_branch_exists? && @merge_request.target_branch_exists? - = render "projects/merge_requests/show/mr_accept" - - else - = render "projects/merge_requests/show/no_accept" - - - if @merge_request.closed? - %h4 - Closed - - if @merge_request.closed_event - by #{link_to_member(@project, @merge_request.closed_event.author, avatar: false)} - #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} - %p Changes were not merged into target branch - - - if @merge_request.merged? - %h4 - Merged - - if @merge_request.merge_event - by #{link_to_member(@project, @merge_request.merge_event.author, avatar: false)} - #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} - = render "projects/merge_requests/show/remove_source_branch" - - - if @merge_request.locked? - %h4 - Merge in progress... - %p - Merging is in progress. While merging this request is locked and cannot be closed. - - - unless @commits.any? - %h4 Nothing to merge - %p - Nothing to merge from - %span.label-branch #{@merge_request.source_branch} - to - %span.label-branch #{@merge_request.target_branch} - %br - Try to use different branches or push new code. - - - if @closes_issues.present? && @merge_request.open? - .mr-widget-footer - %span - %i.fa.fa-check - Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} - = succeed '.' do - != gfm(issues_sentence(@closes_issues)) diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index b4df1d2073..25583b2cc6 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -1,8 +1,3 @@ -- if params[:merge_request_context] - $('.context').html("#{escape_javascript(render partial: 'projects/merge_requests/show/context', locals: { issue: @issue })}"); - $('.context').effect('highlight'); - - new UsersSelect() - - $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}); - merge_request = new MergeRequest(); +$('.context').html("#{escape_javascript(render 'shared/issuable/context', issuable: @merge_request)}"); +$('.context').effect('highlight') +merge_request = new MergeRequest(); diff --git a/app/views/projects/merge_requests/widget/_closed.html.haml b/app/views/projects/merge_requests/widget/_closed.html.haml new file mode 100644 index 0000000000..f3cc0e7e8a --- /dev/null +++ b/app/views/projects/merge_requests/widget/_closed.html.haml @@ -0,0 +1,12 @@ +.mr-state-widget + = render 'projects/merge_requests/widget/heading' + .mr-widget-body + %h4 + Closed + - if @merge_request.closed_event + by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)} + #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} + %p + = succeed '.' do + The changes were not merged into + %span.label-branch= @merge_request.target_branch diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml new file mode 100644 index 0000000000..4d4e2f68f6 --- /dev/null +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -0,0 +1,29 @@ +- if @merge_request.has_ci? + .mr-widget-heading + - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status| + .ci_widget{class: "ci-#{status}", style: "display:none"} + - if status == :success + - status = "passed" + = icon("check-circle") + - else + = icon("circle") + %span CI build #{status} + for #{@merge_request.last_commit_short_sha}. + %span.ci-coverage + = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + + .ci_widget + = icon("spinner spin") + Checking CI status for #{@merge_request.last_commit_short_sha}… + + .ci_widget.ci-not_found{style: "display:none"} + = icon("times-circle") + Could not find CI status for #{@merge_request.last_commit_short_sha}. + + .ci_widget.ci-error{style: "display:none"} + = icon("times-circle") + Could not connect to the CI server. Please check your settings and try again. + + :coffeescript + $ -> + merge_request_widget.getCiStatus() diff --git a/app/views/projects/merge_requests/widget/_locked.html.haml b/app/views/projects/merge_requests/widget/_locked.html.haml new file mode 100644 index 0000000000..78d0783cba --- /dev/null +++ b/app/views/projects/merge_requests/widget/_locked.html.haml @@ -0,0 +1,9 @@ +.mr-state-widget + = render 'projects/merge_requests/widget/heading' + .mr-widget-body + %h4 + = icon("spinner spin") + Merge in progress… + %p + This merge request is in the process of being merged, during which time it is locked and cannot be closed. + diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml new file mode 100644 index 0000000000..d22dfa085b --- /dev/null +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -0,0 +1,49 @@ +.mr-state-widget + = render 'projects/merge_requests/widget/heading' + .mr-widget-body + %h4 + Merged + - if @merge_request.merge_event + by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} + #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} + %div + - if !@merge_request.source_branch_exists? + = succeed '.' do + The changes were merged into + %span.label-branch= @merge_request.target_branch + The source branch has been removed. + + - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) + .remove_source_branch_widget + %p + = succeed '.' do + The changes were merged into + %span.label-branch= @merge_request.target_branch + You can remove the source branch now. + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do + %i.fa.fa-times + Remove Source Branch + + .remove_source_branch_widget.failed.hide + %p + Failed to remove source branch '#{@merge_request.source_branch}'. + + .remove_source_branch_in_progress.hide + %p + = icon('spinner spin') + Removing source branch '#{@merge_request.source_branch}'. Please wait. This page will be automatically reload. + + :coffeescript + $('.remove_source_branch').on 'click', -> + $('.remove_source_branch_widget').hide() + $('.remove_source_branch_in_progress').show() + + $(".remove_source_branch").on "ajax:success", (e, data, status, xhr) -> + location.reload() + + $(".remove_source_branch").on "ajax:error", (e, data, status, xhr) -> + $('.remove_source_branch_widget').hide() + $('.remove_source_branch_in_progress').hide() + $('.remove_source_branch_widget.failed').show() + + diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml new file mode 100644 index 0000000000..8c61e81937 --- /dev/null +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -0,0 +1,29 @@ +.mr-state-widget + = render 'projects/merge_requests/widget/heading' + .mr-widget-body + - if @project.archived? + = render 'projects/merge_requests/widget/open/archived' + - elsif !@project.satellite.exists? + = render 'projects/merge_requests/widget/open/no_satellite' + - elsif @merge_request.commits.blank? + = render 'projects/merge_requests/widget/open/nothing' + - elsif @merge_request.branch_missing? + = render 'projects/merge_requests/widget/open/missing_branch' + - elsif @merge_request.unchecked? + = render 'projects/merge_requests/widget/open/check' + - elsif @merge_request.cannot_be_merged? + = render 'projects/merge_requests/widget/open/conflicts' + - elsif @merge_request.work_in_progress? + = render 'projects/merge_requests/widget/open/wip' + - elsif !@merge_request.can_be_merged_by?(current_user) + = render 'projects/merge_requests/widget/open/not_allowed' + - elsif @merge_request.can_be_merged? + = render 'projects/merge_requests/widget/open/accept' + + - if @closes_issues.present? + .mr-widget-footer + %span + %i.fa.fa-check + Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} + = succeed '.' do + != gfm(issues_sentence(@closes_issues)) diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml new file mode 100644 index 0000000000..263cab7a9e --- /dev/null +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -0,0 +1,20 @@ +- if @merge_request.open? + = render 'projects/merge_requests/widget/open' +- elsif @merge_request.merged? + = render 'projects/merge_requests/widget/merged' +- elsif @merge_request.closed? + = render 'projects/merge_requests/widget/closed' +- elsif @merge_request.locked? + = render 'projects/merge_requests/widget/locked' + +:javascript + var merge_request_widget; + + merge_request_widget = new MergeRequestWidget({ + url_to_automerge_check: "#{automerge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + check_enable: #{@merge_request.unchecked? ? "true" : "false"}, + url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_enable: #{@project.ci_service ? "true" : "false"}, + current_status: "#{@merge_request.automerge_status}", + }); + diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml new file mode 100644 index 0000000000..df20205de1 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -0,0 +1,25 @@ += form_for [:automerge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f| + = hidden_field_tag :authenticity_token, form_authenticity_token + .accept-merge-holder.clearfix.js-toggle-container + .accept-action + = f.button class: "btn btn-create accept_merge_request" do + Accept Merge Request + - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? + .accept-control.checkbox + = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do + = check_box_tag :should_remove_source_branch + Remove source branch + .accept-control + = link_to "#", class: "modify-merge-commit-link js-toggle-button" do + = icon('edit') + Modify commit message + .js-toggle-content.hide.prepend-top-20 + = render 'shared/commit_message_container', params: params, + text: @merge_request.merge_commit_message, + rows: 14, hint: true + + :coffeescript + $('.accept-mr-form').on 'ajax:before', -> + btn = $('.accept_merge_request') + btn.disable() + btn.html(" Merge in progress") diff --git a/app/views/projects/merge_requests/widget/open/_archived.html.haml b/app/views/projects/merge_requests/widget/open/_archived.html.haml new file mode 100644 index 0000000000..ab30fa6b24 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_archived.html.haml @@ -0,0 +1,4 @@ +%h4 + Project is archived +%p + This merge request cannot be merged because archived projects cannot be written to. diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml new file mode 100644 index 0000000000..b6b8974297 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -0,0 +1,7 @@ +%strong + = icon("spinner spin") + Checking ability to merge automatically… + +:coffeescript + $ -> + merge_request_widget.getMergeStatus() diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml new file mode 100644 index 0000000000..e6c089fefb --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -0,0 +1,10 @@ +%h4 + = icon("exclamation-triangle") + This merge request contains merge conflicts + +%p + Please resolve these conflicts or + - if @merge_request.can_be_merged_by?(current_user) + #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}. + - else + ask someone with write access to this repository to merge this request manually. diff --git a/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml b/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml new file mode 100644 index 0000000000..c9f0762949 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml @@ -0,0 +1,16 @@ +- unless @merge_request.source_branch_exists? + %h4 + = icon("exclamation-triangle") + Source branch + %span.label-branch= source_branch_with_namespace(@merge_request) + does not exist + %p + Please restore the source branch or close this merge request and open a new merge request with a different source branch. +- else + %h4 + = icon("exclamation-triangle") + Target branch + %span.label-branch= @merge_request.target_branch + does not exist + %p + Please restore the target branch or use a different target branch. diff --git a/app/views/projects/merge_requests/widget/open/_no_satellite.html.haml b/app/views/projects/merge_requests/widget/open/_no_satellite.html.haml new file mode 100644 index 0000000000..3718cfd833 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_no_satellite.html.haml @@ -0,0 +1,3 @@ +%p + %span + %strong This repository does not have a satellite. Please ask an administrator to fix this issue! diff --git a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml new file mode 100644 index 0000000000..a8145558ca --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml @@ -0,0 +1,4 @@ +%h4 + Ready to be merged automatically +%p + Ask someone with write access to this repository to merge this request. diff --git a/app/views/projects/merge_requests/widget/open/_nothing.html.haml b/app/views/projects/merge_requests/widget/open/_nothing.html.haml new file mode 100644 index 0000000000..35626b624b --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_nothing.html.haml @@ -0,0 +1,8 @@ +%h4 + = icon("exclamation-triangle") + Nothing to merge from + %span.label-branch= source_branch_with_namespace(@merge_request) + into + %span.label-branch= @merge_request.target_branch +%p + Please push new commits to the source branch or use a different target branch. diff --git a/app/views/projects/merge_requests/widget/open/_reload.html.haml b/app/views/projects/merge_requests/widget/open/_reload.html.haml new file mode 100644 index 0000000000..acfc31725e --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_reload.html.haml @@ -0,0 +1,6 @@ +%h4 + = icon("exclamation-triangle") + This merge request failed to be merged automatically + +%p + Please reload the page to find out the reason. diff --git a/app/views/projects/merge_requests/widget/open/_wip.html.haml b/app/views/projects/merge_requests/widget/open/_wip.html.haml new file mode 100644 index 0000000000..0cf16542cc --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_wip.html.haml @@ -0,0 +1,5 @@ +%h4 + This merge request is currently a Work In Progress + +%p + When this merge request is ready, remove the "WIP" prefix from the title to allow it to be merged. diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 95b7070ce5..b93462e5bd 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -5,7 +5,7 @@ %hr -= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f| -if @milestone.errors.any? .alert.alert-danger %ul @@ -16,7 +16,7 @@ .form-group = f.label :title, "Title", class: "control-label" .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control" + = f.text_field :title, maxlength: 255, class: "form-control", required: true %p.hint Required .form-group.milestone-description = f.label :description, "Description", class: "control-label" @@ -45,10 +45,7 @@ :javascript - disableButtonIfEmptyField("#milestone_title", ".btn-save"); $( ".datepicker" ).datepicker({ dateFormat: "yy-mm-dd", onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); - - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 62360158ff..2ce5358fa7 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -5,6 +5,10 @@ %i.fa.fa-pencil-square-o Edit = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close" + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-sm btn-remove" do + %i.fa.fa-trash-o + Remove + %h4 = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - if milestone.expired? and not milestone.closed? @@ -13,10 +17,10 @@ = milestone.expires_at .row .col-sm-6 - = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do + = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do = pluralize milestone.issues.count, 'Issue'   - = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do + = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do = pluralize milestone.merge_requests.count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index b1bc3ba0eb..c09815a212 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1 +1,2 @@ +- page_title "Edit", @milestone.title, "Milestones" = render "form" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index d3eab8d6d7..995eecd783 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Milestones" .pull-right - if can? current_user, :admin_milestone, @project = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index b1bc3ba0eb..47149dfea4 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1 +1,2 @@ +- page_title "New Milestone" = render "form" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 25cc003096..7b1681df33 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,3 +1,4 @@ +- page_title @milestone.title, "Milestones" %h4.page-title .issue-box{ class: issue_box_class(@milestone) } - if @milestone.closed? @@ -18,6 +19,9 @@ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" - else = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do + %i.fa.fa-trash-o + Remove %hr - if @milestone.issues.any? && @milestone.can_be_closed? @@ -60,12 +64,13 @@ Participants %span.badge= @users.count - - if @project.issues_enabled - .pull-right + .pull-right + - if can?(current_user, :create_issue, @project) = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do %i.fa.fa-plus New Issue - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped" + - if can?(current_user, :read_issue, @project) + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" .tab-content .tab-pane.active#tab-issues @@ -84,7 +89,7 @@ .col-md-3 = render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing') .col-md-3 - = render('merge_requests', title: 'Declined (closed)', merge_requests: @merge_requests.declined, id: 'closed') + = render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.closed, id: 'closed') .col-md-3 .panel.panel-primary .panel-heading Merged diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index c36bad1e94..52b5b8b877 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,10 +1,11 @@ +- page_title "Network", @ref = render "head" .project-network .controls = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' - = button_tag class: 'btn btn-success btn-search-sha' do - %i.fa.fa-search + = button_tag class: 'btn btn-success' do + = icon('search') .inline.prepend-left-20 .checkbox.light = label_tag :filter_ref do @@ -15,12 +16,10 @@ = spinner nil, true :javascript - disableButtonIfEmptyField('#extended_sha1', '.btn-search-sha') - network_graph = new Network({ - url: '#{namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))}', - commit_url: '#{namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")}', - ref: '#{@ref}', + url: "#{escape_javascript(@url)}", + commit_url: "#{escape_javascript(@commit_url)}", + ref: "#{escape_javascript(@ref)}", commit_id: '#{@commit.id}' }) new ShortcutsNetwork(network_graph.branch_graph) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a06c85b425..d25fe68242 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,15 +1,17 @@ +- page_title 'New Project' +- header_title 'New Project' .project-edit-container .project-edit-errors = render 'projects/errors' .project-edit-content - = form_for @project, html: { class: 'new_project form-horizontal' } do |f| + = form_for @project, html: { class: 'new_project form-horizontal js-requires-input' } do |f| .form-group.project-name-holder = f.label :path, class: 'control-label' do %strong Project path .col-sm-10 .input-group - = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true, required: true .input-group-addon \.git @@ -38,7 +40,7 @@ - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: 'btn' do + = link_to status_import_bitbucket_path, class: 'btn', "data-no-turbolink" => "true" do %i.fa.fa-bitbucket Bitbucket - else @@ -83,7 +85,7 @@ %li The import will time out after 4 minutes. For big repositories, use a clone/push combination. %li - To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}. + To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. %hr.prepend-botton-10 @@ -93,7 +95,7 @@ %span.light (optional) .col-sm-10 = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250, tabindex: 3 - = render "visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true + = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project .form-actions = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index 789f3e19fd..c6726cbafa 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -1,34 +1,34 @@ -- note1 = notes1.present? ? notes1.first : nil -- note2 = notes2.present? ? notes2.first : nil +- note1 = notes_left.present? ? notes_left.first : nil +- note2 = notes_right.present? ? notes_right.first : nil %tr.notes_holder - if note1 - %td.notes_line + %td.notes_line.old %span.btn.disabled %i.fa.fa-comment - = notes1.count - %td.notes_content.parallel + = notes_left.count + %td.notes_content.parallel.old %ul.notes{ rel: note1.discussion_id } - = render notes1 + = render notes_left .discussion-reply-holder - = link_to_reply_diff(note1) + = link_to_reply_diff(note1, 'old') - else - %td= "" - %td= "" + %td.notes_line.old= "" + %td.notes_content.parallel.old= "" - if note2 - %td.notes_line + %td.notes_line.new %span.btn.disabled %i.fa.fa-comment - = notes2.count - %td.notes_content.parallel + = notes_right.count + %td.notes_content.parallel.new %ul.notes{ rel: note2.discussion_id } - = render notes2 + = render notes_right .discussion-reply-holder - = link_to_reply_diff(note2) + = link_to_reply_diff(note2, 'new') - else - %td= "" - %td= "" + %td.notes_line.new= "" + %td.notes_content.parallel.new= "" diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index acb3991d29..8f7d2e84c7 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,15 +1,11 @@ .note-edit-form = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f| = note_target_fields(note) - = render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do - = render 'projects/zen', f: f, attr: :note, - classes: 'note_text js-note-text' - - .comment-hints.clearfix - .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} - .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. + = render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' + = render 'projects/notes/hints' .note-form-actions .buttons - = f.submit 'Save Comment', class: "btn btn-primary btn-save btn-grouped js-comment-button" - = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel" \ No newline at end of file + = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button' + = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 2ada6cb670..3be8f44b28 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,24 +1,19 @@ = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| + = hidden_field_tag :view, params[:view] + = hidden_field_tag :line_type = note_target_fields(@note) = f.hidden_field :commit_id = f.hidden_field :line_code = f.hidden_field :noteable_id = f.hidden_field :noteable_type - = render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do - = render 'projects/zen', f: f, attr: :note, - classes: 'note_text js-note-text' - - .comment-hints.clearfix - .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} - .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. + = render layout: 'projects/md_preview', locals: { preview_class: "note-text", referenced_users: true } do + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' + = render 'projects/notes/hints' .error-alert .note-form-actions - .buttons + .buttons.clearfix = f.submit 'Add Comment', class: "btn comment-btn btn-grouped js-comment-button" = yield(:note_actions) %a.btn.grouped.js-close-discussion-note-form Cancel - -:javascript - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml new file mode 100644 index 0000000000..6e7929bdab --- /dev/null +++ b/app/views/projects/notes/_hints.html.haml @@ -0,0 +1,9 @@ +.comment-hints.clearfix + .pull-left + = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 + tip: + = random_markdown_tip + .pull-right + = link_to '#', class: 'markdown-selector', tabindex: -1 do + = icon('paperclip') + Attach a file diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 0728f8fa42..de75d44fc4 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -2,55 +2,67 @@ .timeline-entry-inner .timeline-icon - if note.system - %span.fa.fa-circle + %span= icon('circle') - else = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: "avatar s40", alt: '' + = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: '' .timeline-content .note-header - .note-actions - = link_to "##{dom_id(note)}", name: dom_id(note) do - %i.fa.fa-link - Link here -   - - if can?(current_user, :admin_note, note) && note.editable? - = link_to "#", title: "Edit comment", class: "js-note-edit" do - %i.fa.fa-pencil-square-o - Edit -   - = link_to namespace_project_note_path(@project.namespace, @project, note), title: "Remove comment", method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: "danger js-note-delete" do - %i.fa.fa-trash-o.cred - Remove + - if note_editable?(note) + .note-actions + = link_to '#', title: 'Edit comment', class: 'js-note-edit' do + = icon('pencil-square-o') + + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do + = icon('trash-o') + + - unless note.system + - member = note.project.team.find_member(note.author.id) + - if member + %span.note-role.label + = member.human_access + - if note.system = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: "avatar s16", alt: '' - = link_to_member(@project, note.author, avatar: false) + = image_tag avatar_icon(note.author_email), class: 'avatar s16', alt: '' + + = link_to_member(note.project, note.author, avatar: false) + %span.author-username = '@' + note.author.username + %span.note-last-update - = note_timestamp(note) + = link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') + - if note.updated_at != note.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago') + - if note.updated_by && note.updated_by != note.author + by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)} - if note.superceded?(@notes) - if note.upvote? %span.vote.upvote.label.label-gray.strikethrough - %i.fa.fa-thumbs-up + = icon('thumbs-up') \+1 - if note.downvote? %span.vote.downvote.label.label-gray.strikethrough - %i.fa.fa-thumbs-down + = icon('thumbs-down') \-1 - else - if note.upvote? %span.vote.upvote.label.label-success - %i.fa.fa-thumbs-up + = icon('thumbs-up') \+1 - if note.downvote? %span.vote.downvote.label.label-danger - %i.fa.fa-thumbs-down + = icon('thumbs-down') \-1 - .note-body + .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do = markdown(note.note, {no_header_anchors: true}) @@ -62,10 +74,10 @@ = link_to note.attachment.url, target: '_blank' do = image_tag note.attachment.url, class: 'note-image-attach' .attachment - = link_to note.attachment.url, target: "_blank" do - %i.fa.fa-paperclip + = link_to note.attachment.url, target: '_blank' do + = icon('paperclip') = note.attachment_identifier - = link_to delete_attachment_namespace_project_note_path(@project.namespace, @project, note), - title: "Delete this attachment", method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: "danger js-note-attachment-delete" do - %i.fa.fa-trash-o.cred + = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), + title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do + = icon('trash-o', class: 'cred') .clear diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 813e37276b..04222b8f7c 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -3,8 +3,8 @@ .js-notes-busy .js-main-target-form -- if can? current_user, :write_note, @project - = render "projects/notes/form" +- if can? current_user, :create_note, @project + = render "projects/notes/form", view: params[:view] :javascript - new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}) + new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{params[:view]}") diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index 7c6f724317..4f15a99d06 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -14,7 +14,9 @@ - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) + %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + .discussion-body.js-toggle-content = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 62609cfc1c..6903fad4a0 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -14,7 +14,7 @@ last updated by = link_to_member(@project, last_note.author, avatar: false) %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content - if note.for_diff_line? = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 711aa39101..0301445b5b 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -12,18 +12,19 @@ .diff-content %table - note.truncated_diff_lines.each do |line| + - type = line.type - line_code = generate_line_code(note.file_path, line) - %tr.line_holder{ id: line_code } - - if line.type == "match" + %tr.line_holder{ id: line_code, class: "#{type}" } + - if type == "match" %td.old_line= "..." %td.new_line= "..." %td.line_content.matched= line.text - else - %td.old_line{class: line.type == "new" ? "new" : "old"} - = raw(line.type == "new" ? " " : line.old_pos) - %td.new_line{class: line.type == "new" ? "new" : "old"} - = raw(line.type == "old" ? " " : line.new_pos) - %td.line_content{class: "noteable_line #{line.type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) + %td.old_line + = raw(type == "new" ? " " : line.old_pos) + %td.new_line + = raw(type == "old" ? " " : line.new_pos) + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml index 52a1d342f5..218b0da397 100644 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ b/app/views/projects/notes/discussions/_outdated.html.haml @@ -14,6 +14,6 @@ last updated by = link_to_member(@project, last_note.author, avatar: false) %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content.hide = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 635e4d7094..860a997cff 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -38,8 +38,9 @@   - if current_user == user - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do - %i.fa.fa-minus.fa-inverse + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do + = icon("sign-out") + Leave - else = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 293754cd0c..6914543f6d 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -1,3 +1,4 @@ +- page_title "Import members" %h3.page-title Import members from another project %p.light diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 36a6f6a155..162583e4b1 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Members" %h3.page-title Users with access to this project @@ -10,7 +11,7 @@ .clearfix.js-toggle-container = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' } + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } = button_tag 'Search', class: 'btn' - if can?(current_user, :admin_project_member, @project) diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index a3464c0e5e..52b3a50c1e 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Protected branches" %h3.page-title Protected branches %p.light Keep stable branches secure and force developers to use Merge Requests %hr diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 35c15cf3a9..db7f244d00 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -11,9 +11,11 @@ - if @logs.present? :plain var current_url = location.href.replace(/\/?$/, '/'); - var log_url = '#{namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))}'.replace(/\/?$/, '/'); + var log_url = "#{escape_javascript(@log_url)}".replace(/\/?$/, '/'); + if(current_url == log_url) { - // Load 10 more commit log for each file in tree + // Load more commit logs for each file in tree // if we still on the same page - ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))}'); + var url = "#{escape_javascript(@more_log_url)}"; + ajaxGet(url); } diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index ce6b7a0737..e1823b5119 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -10,96 +10,11 @@ %hr -= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| - - if @service.errors.any? - .alert.alert-danger - %ul - - @service.errors.full_messages.each do |msg| - %li= msg - - - if @service.help.present? - .well - = preserve do - = markdown @service.help - - .form-group - = f.label :active, "Active", class: "control-label" - .col-sm-10 - = f.check_box :active - - - if @service.supported_events.length > 1 - .form-group - = f.label :url, "Trigger", class: 'control-label' - .col-sm-10 - - if @service.supported_events.include?("push") - %div - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This url will be triggered by a push to the repository - - if @service.supported_events.include?("tag_push") - %div - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - - if @service.supported_events.include?("note") - %div - = f.check_box :note_events, class: 'pull-left' - .prepend-left-20 - = f.label :note_events, class: 'list-label' do - %strong Comments - %p.light - This url will be triggered when someone adds a comment - - if @service.supported_events.include?("issue") - %div - = f.check_box :issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This url will be triggered when an issue is created - - if @service.supported_events.include?("merge_request") - %div - = f.check_box :merge_requests_events, class: 'pull-left' - .prepend-left-20 - = f.label :merge_requests_events, class: 'list-label' do - %strong Merge Request events - %p.light - This url will be triggered when a merge request is created - - - @service.fields.each do |field| - - name = field[:name] - - title = field[:title] || name.humanize - - value = service_field_value(field[:type], @service.send(name)) - - type = field[:type] - - placeholder = field[:placeholder] - - choices = field[:choices] - - default_choice = field[:default_choice] - - help = field[:help] - - .form-group - = f.label name, title, class: "control-label" - .col-sm-10 - - if type == 'text' - = f.text_field name, class: "form-control", placeholder: placeholder - - elsif type == 'textarea' - = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder - - elsif type == 'checkbox' - = f.check_box name - - elsif type == 'select' - = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - - elsif type == 'password' - = f.password_field name, placeholder: value, class: 'form-control' - - if help - %span.help-block= help += form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| + = render 'shared/service_settings', form: form .form-actions - = f.submit 'Save', class: 'btn btn-save' + = form.submit 'Save', class: 'btn btn-save'   - if @service.valid? && @service.activated? - disabled = @service.can_test? ? '':'disabled' diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml index bcc5832792..50ed78286d 100644 --- a/app/views/projects/services/edit.html.haml +++ b/app/views/projects/services/edit.html.haml @@ -1 +1,2 @@ +- page_title @service.title, "Services" = render 'form' diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml index 0d3ccb6bb8..1065def693 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Services" %h3.page-title Project services %p.light Project services allow you to integrate GitLab with other applications diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder new file mode 100644 index 0000000000..242684e5c7 --- /dev/null +++ b/app/views/projects/show.atom.builder @@ -0,0 +1,12 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do + xml.title "#{@project.name} activity" + xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" + xml.id namespace_project_url(@project.namespace, @project) + xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + + @events.each do |event| + event_to_atom(xml, event) + end +end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4464c51744..ebbd3e477f 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,108 +1,79 @@ += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") + - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' = render 'shared/no_password' +- if prefer_readme? + = render 'projects/last_push' + = render "home_panel" -%ul.nav.nav-tabs - %li.active - = link_to '#tab-activity', 'data-toggle' => 'tab' do - Activity - - if @repository.readme +.project-stats + %ul.nav.nav-pills %li - = link_to '#tab-readme', 'data-toggle' => 'tab' do - Readme - - if @repository.changelog + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do + = pluralize(number_with_delimiter(@project.commit_count), 'commit') %li - = link_to changelog_url(@project) do - Changelog - - if @repository.contribution_guide + = link_to namespace_project_branches_path(@project.namespace, @project) do + = pluralize(number_with_delimiter(@repository.branch_names.count), 'branch') %li - = link_to contribution_guide_url(@project) do - Contribution guide - - if @repository.license - %li - = link_to license_url(@project) do - License + = link_to namespace_project_tags_path(@project.namespace, @project) do + = pluralize(number_with_delimiter(@repository.tag_names.count), 'tag') - .project-home-links - - unless @project.empty_repo? - = link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) - = link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), namespace_project_branches_path(@project.namespace, @project) - = link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), namespace_project_tags_path(@project.namespace, @project) - %span.light.prepend-left-20= repository_size + - if !prefer_readme? && @repository.readme + %li + = link_to 'Readme', readme_path(@project) -.tab-content - .tab-pane.active#tab-activity - .row - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left - %section.col-md-9 - = render "events/event_last_push", event: @last_push - = render 'shared/event_filter' - .content_list - = spinner - %aside.col-md-3.project-side - .clearfix - - if @project.archived? - .alert.alert-warning - %h4 - %i.fa.fa-exclamation-triangle - Archived project! - %p Repository is read-only + - if @repository.changelog + %li + = link_to 'Changelog', changelog_path(@project) - - if @project.forked_from_project - .well - %i.fa.fa-code-fork.project-fork-icon - Forked from: - %br - = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) + - if @repository.license + %li + = link_to 'License', license_path(@project) - - unless @project.empty_repo? - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do - %i.fa.fa-exchange - Compare code + - if @repository.contribution_guide + %li + = link_to 'Contribution guide', contribution_guide_path(@project) - - if can?(current_user, :download_code, @project) - = render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-block' + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_changelog_path(@project) do + Add Changelog + - unless @repository.license + %li.missing + = link_to add_license_path(@project) do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_contribution_guide_path(@project) do + Add Contribution guide - - if version = @repository.version - - detail_url = changelog_url(@project) || version_url(@project) - = link_to detail_url, class: 'btn btn-block' do - %i.fa.fa-file-text-o - Version: - %span.count - = @repository.blob_by_oid(version.id).data +- if @project.archived? + .text-warning.center.prepend-top-20 + %p + = icon("exclamation-triangle fw") + Archived project! Repository is read-only - .prepend-top-10.append-bottom-10 - %p - %span.light Created on - #{@project.created_at.stamp('Aug 22, 2013')} - %p - %span.light Owned by #{@project.group ? "the" : nil} - - if @project.group - #{link_to @project.group.name, @project.group} group - - else - #{link_to @project.owner_name, @project.owner} +%hr +%section + - if prefer_readme? + = render 'projects/readme' + - else + = render 'projects/activity' - .prepend-top-10 - - @project.ci_services.each do |ci_service| - - if ci_service.active? && ci_service.respond_to?(:builds_path) - %hr - - if ci_service.respond_to?(:status_img_path) - = link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do - = image_tag ci_service.status_img_path, alt: "build status" - - else - %span.light CI provided by - = link_to ci_service.title, ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' - - - if readme = @repository.readme - .tab-pane#tab-readme - %article.readme-holder#README - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do - %h4.readme-file-title - %i.fa.fa-file - = readme.name - .wiki - = render_readme(readme) +- if current_user + - access = user_max_access_in_project(current_user, @project) + - if access + %hr + %p.light + You have #{access} access to this project. + - if @project.project_member_by_id(current_user) + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), + data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do + Leave this project diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index 2d4d5d030a..945f0084df 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -1,4 +1,5 @@ +- page_title "Edit", @snippet.title, "Snippets" %h3.page-title Edit snippet %hr -= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet) += render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index e2d8ec673a..30081673ff 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,6 +1,7 @@ +- page_title "Snippets" %h3.page-title Snippets - - if can? current_user, :write_project_snippet, @project + - if can? current_user, :create_project_snippet, @project = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Snippet" do Add new snippet diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index bb659dba0c..e38d95c45e 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -1,4 +1,5 @@ +- page_title "New Snippets" %h3.page-title New snippet %hr -= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet) += render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index d19689a105..8cbb813c75 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,3 +1,4 @@ +- page_title @snippet.title, "Snippets" %h3.page-title = @snippet.title @@ -27,7 +28,7 @@ = @snippet.file_name .file-actions .btn-group - - if can?(current_user, :modify_project_snippet, @snippet) + - if can?(current_user, :update_project_snippet, @snippet) = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet' = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - if can?(current_user, :admin_project_snippet, @snippet) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index f1bc2bc9a2..d4652a47cb 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Tags" = render "projects/commits/head" %h3.page-title diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 655044438d..172fafdeef 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -1,3 +1,4 @@ +- page_title "New Tag" - if @error .alert.alert-danger %button{ type: "button", class: "close", "data-dismiss" => "alert"} × diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index d304690d16..5048154cb2 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -49,5 +49,5 @@ :javascript // Load last commit log for each file in tree $('#tree-slider').waitForImages(function() { - ajaxGet('#{@logs_path}'); + ajaxGet("#{escape_javascript(@logs_path)}"); }); diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index 50521264a6..a3a4bd4f75 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,3 +1,2 @@ %span.str-truncated - %span.tree_author= commit_author_link(commit, avatar: true, size: 16) = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index feca145369..c9e59428e7 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,9 +1,16 @@ +- page_title @path.presence || "Files", @ref += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") + += render 'projects/last_push' + .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path - if can? current_user, :download_code, @project .tree-download-holder - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-sm pull-right hidden-xs hidden-sm', split_button: true + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true #tree-holder.tree-holder.clearfix = render "tree", tree: @tree diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index 4f3f4cab8d..7d9bd08385 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -6,4 +6,4 @@ $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); - $('.project-edit-content .btn-save').enableButton(); + $('.project-edit-content .btn-save').enable(); diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 9fbfa0b1ae..904600499a 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -12,8 +12,7 @@ = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control" .row - .col-sm-2 - .col-sm-10 + .col-sm-offset-2.col-sm-10 %p.cgray To link to a (new) page you can just type %code [Link Title](page-slug) @@ -41,6 +40,3 @@ - else = f.submit 'Create page', class: "btn-create btn" = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" - -:javascript - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 633214a4e8..788bb8cf1e 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -2,7 +2,7 @@ - if (@page && @page.persisted?) = link_to history_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do Page History - - if can?(current_user, :write_wiki, @project) + - if can?(current_user, :create_wiki, @project) = link_to edit_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 693c3facb3..804a1b52db 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -10,7 +10,7 @@ %i.fa.fa-download Git Access - - if can?(current_user, :write_wiki, @project) + - if can?(current_user, :create_wiki, @project) .pull-right = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do %i.fa.fa-plus diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 6834969de8..dace172438 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -1,4 +1,4 @@ -%div#modal-new-wiki.modal.hide +%div#modal-new-wiki.modal .modal-dialog .modal-content .modal-header @@ -8,6 +8,8 @@ = label_tag :new_wiki_path do %span Page slug = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project) + %p.hidden.text-danger{data: { error: "slug" }} + The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and / %p.hint Please don't use spaces. .modal-footer diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 566850cb78..3f1dce1050 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,3 +1,4 @@ +- page_title "Edit", @page.title, "Wiki" = render 'nav' .pull-right = render 'main_links' diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml index 48058124f9..ead9941240 100644 --- a/app/views/projects/wikis/empty.html.haml +++ b/app/views/projects/wikis/empty.html.haml @@ -1,3 +1,4 @@ +- page_title "Wiki" %h3.page-title Empty page %hr .error_message diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 365edb524f..fd266baf66 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,3 +1,4 @@ +- page_title "Git Access", "Wiki" = render 'nav' .row .col-sm-6 @@ -19,7 +20,7 @@ %pre.dark :preserve git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} - cd #{@project_wiki.path} + cd #{h @project_wiki.path} %legend Start Gollum And Edit Locally: %pre.dark diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 91291f753f..673ec2d20e 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,3 +1,4 @@ +- page_title "History", @page.title, "Wiki" = render 'nav' %h3.page-title %span.light History for diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index ee233d9086..890ff1aed7 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,3 +1,4 @@ +- page_title "All Pages", "Wiki" = render 'nav' %h3.page-title All Pages diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index a6263e93f6..5c4dd7f91a 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,7 +1,12 @@ +- page_title @page.title, "Wiki" = render 'nav' %h3.page-title = @page.title = render 'main_links' + +.wiki-last-edit-by + Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + - if @page.historical? .warning_message This is an old version of this page. @@ -15,6 +20,6 @@ = render_wiki_content(@page) %hr - .wiki-last-edit-by Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml new file mode 100644 index 0000000000..154332cb9a --- /dev/null +++ b/app/views/search/_category.html.haml @@ -0,0 +1,77 @@ +%ul.nav.nav-pills.search-filter + - if @project + %li{class: ("active" if @scope == 'blobs')} + = link_to search_filter_path(scope: 'blobs') do + = icon('code fw') + %span + Code + %span.badge + = @search_results.blobs_count + %li{class: ("active" if @scope == 'issues')} + = link_to search_filter_path(scope: 'issues') do + = icon('exclamation-circle fw') + %span + Issues + %span.badge + = @search_results.issues_count + %li{class: ("active" if @scope == 'merge_requests')} + = link_to search_filter_path(scope: 'merge_requests') do + = icon('tasks fw') + %span + Merge requests + %span.badge + = @search_results.merge_requests_count + %li{class: ("active" if @scope == 'notes')} + = link_to search_filter_path(scope: 'notes') do + = icon('comments fw') + %span + Comments + %span.badge + = @search_results.notes_count + %li{class: ("active" if @scope == 'wiki_blobs')} + = link_to search_filter_path(scope: 'wiki_blobs') do + = icon('book fw') + %span + Wiki + %span.badge + = @search_results.wiki_blobs_count + + - elsif @show_snippets + %li{class: ("active" if @scope == 'snippet_blobs')} + = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do + = icon('code fw') + %span + Snippet Contents + %span.badge + = @search_results.snippet_blobs_count + %li{class: ("active" if @scope == 'snippet_titles')} + = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do + = icon('book fw') + %span + Titles and Filenames + %span.badge + = @search_results.snippet_titles_count + + - else + %li{class: ("active" if @scope == 'projects')} + = link_to search_filter_path(scope: 'projects') do + = icon('bookmark fw') + %span + Projects + %span.badge + = @search_results.projects_count + %li{class: ("active" if @scope == 'issues')} + = link_to search_filter_path(scope: 'issues') do + = icon('exclamation-circle fw') + %span + Issues + %span.badge + = @search_results.issues_count + %li{class: ("active" if @scope == 'merge_requests')} + = link_to search_filter_path(scope: 'merge_requests') do + = icon('tasks fw') + %span + Merge requests + %span.badge + = @search_results.merge_requests_count + diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index ffc145497a..e2d0cab9e7 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -1,5 +1,5 @@ .dropdown.inline - %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn{type: 'button', 'data-toggle' => 'dropdown'} %i.fa.fa-tags %span.light Group: - if @group.present? @@ -17,7 +17,7 @@ = group.name .dropdown.inline.prepend-left-10.project-filter - %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn{type: 'button', 'data-toggle' => 'dropdown'} %i.fa.fa-tags %span.light Project: - if @project.present? diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml new file mode 100644 index 0000000000..5ee70be1ad --- /dev/null +++ b/app/views/search/_form.html.haml @@ -0,0 +1,12 @@ += form_tag search_path, method: :get, class: 'form-inline' do |f| + = hidden_field_tag :project_id, params[:project_id] + = hidden_field_tag :group_id, params[:group_id] + = hidden_field_tag :snippets, params[:snippets] + = hidden_field_tag :scope, params[:scope] + .search-holder.clearfix + .form-group + = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true + = button_tag 'Search', class: "btn btn-primary" + - unless params[:snippets].eql? 'true' + .pull-right + = render 'filter' diff --git a/app/views/search/_global_filter.html.haml b/app/views/search/_global_filter.html.haml deleted file mode 100644 index 442bd84f93..0000000000 --- a/app/views/search/_global_filter.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%ul.nav.nav-pills.nav-stacked.search-filter - %li{class: ("active" if @scope == 'projects')} - = link_to search_filter_path(scope: 'projects') do - Projects - .pull-right - = @search_results.projects_count - %li{class: ("active" if @scope == 'issues')} - = link_to search_filter_path(scope: 'issues') do - Issues - .pull-right - = @search_results.issues_count - %li{class: ("active" if @scope == 'merge_requests')} - = link_to search_filter_path(scope: 'merge_requests') do - Merge requests - .pull-right - = @search_results.merge_requests_count diff --git a/app/views/search/_project_filter.html.haml b/app/views/search/_project_filter.html.haml deleted file mode 100644 index ad933502a2..0000000000 --- a/app/views/search/_project_filter.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -%ul.nav.nav-pills.nav-stacked.search-filter - %li{class: ("active" if @scope == 'blobs')} - = link_to search_filter_path(scope: 'blobs') do - %i.fa.fa-code - Code - .pull-right - = @search_results.blobs_count - %li{class: ("active" if @scope == 'issues')} - = link_to search_filter_path(scope: 'issues') do - %i.fa.fa-exclamation-circle - Issues - .pull-right - = @search_results.issues_count - %li{class: ("active" if @scope == 'merge_requests')} - = link_to search_filter_path(scope: 'merge_requests') do - %i.fa.fa-code-fork - Merge requests - .pull-right - = @search_results.merge_requests_count - %li{class: ("active" if @scope == 'notes')} - = link_to search_filter_path(scope: 'notes') do - %i.fa.fa-comments - Comments - .pull-right - = @search_results.notes_count - %li{class: ("active" if @scope == 'wiki_blobs')} - = link_to search_filter_path(scope: 'wiki_blobs') do - %i.fa.fa-book - Wiki - .pull-right - = @search_results.wiki_blobs_count - diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 796dd752a4..741c780ad9 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,28 +1,21 @@ -%h4 - #{@search_results.total_count} results found - - unless @show_snippets - - if @project - for #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} - - elsif @group - for #{link_to @group.name, @group} +- if @search_results.empty? + = render partial: "search/results/empty" +- else + .light + Search results for + %code + = @search_term + - unless @show_snippets + - if @project + in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} + - elsif @group + in group #{link_to @group.name, @group} -%hr - -.row - .col-sm-3 - - if @project - = render "project_filter" - - elsif @show_snippets - = render 'snippet_filter' - - else - = render "global_filter" - .col-sm-9 + %br + .results.prepend-top-10 .search-results - - if @search_results.empty? - = render partial: "search/results/empty", locals: { message: "We couldn't find any matching results" } - - else - = render partial: "search/results/#{@scope.singularize}", collection: @objects - = paginate @objects, theme: 'gitlab' + = render partial: "search/results/#{@scope.singularize}", collection: @objects + = paginate @objects, theme: 'gitlab' :javascript $(".search-results .term").highlight("#{escape_javascript(params[:search])}"); diff --git a/app/views/search/_snippet_filter.html.haml b/app/views/search/_snippet_filter.html.haml deleted file mode 100644 index 95d23fa9f4..0000000000 --- a/app/views/search/_snippet_filter.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%ul.nav.nav-pills.nav-stacked.search-filter - %li{class: ("active" if @scope == 'snippet_blobs')} - = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do - %i.fa.fa-code - Snippet Contents - .pull-right - = @search_results.snippet_blobs_count - %li{class: ("active" if @scope == 'snippet_titles')} - = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do - %i.fa.fa-book - Titles and Filenames - .pull-right - = @search_results.snippet_titles_count diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index 84e9be82c4..58f58eff54 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,3 +1,4 @@ +- blob = @project.repository.parse_search_result(blob) .blob-result .file-holder .file-title diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml index 01fb8cd9b8..05a63016c0 100644 --- a/app/views/search/results/_empty.html.haml +++ b/app/views/search/results/_empty.html.haml @@ -1,4 +1,6 @@ .search_box .search_glyph - %span.fa.fa-search - %h4 #{message} + %h4 + = icon('search') + We couldn't find any results matching + %code #{@search_term} diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 8af393777f..9509985391 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -13,16 +13,7 @@ .file-title %i.fa.fa-file %strong= snippet_blob[:snippet_object].file_name - - if gitlab_markdown?(snippet_blob[:snippet_object].file_name) - .file-content.wiki - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - = preserve do - = markdown(snippet[:data]) - - else - .file-content.code - .nothing-here-block Empty file - - elsif markup?(snippet_blob[:snippet_object].file_name) + - if markup?(snippet_blob[:snippet_object].file_name) .file-content.wiki - snippet_blob[:snippet_chunks].each do |snippet| - unless snippet[:data].empty? diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index f9c5810e3d..c03438eb95 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,3 +1,4 @@ +- wiki_blob = @project.repository.parse_search_result(wiki_blob) .blob-result .file-holder .file-title diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 5b4816e4c4..60f9e9ac9d 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,22 +1,7 @@ -= form_tag search_path, method: :get, class: 'form-horizontal' do |f| - .search-holder.clearfix - .form-group - = label_tag :search, class: 'control-label' do - %span Looking for - .col-sm-6 - = search_field_tag :search, params[:search], placeholder: "issue 143", class: "form-control search-text-input", id: "dashboard_search" - .col-sm-4 - = button_tag 'Search', class: "btn btn-create" - .form-group - .col-sm-2 - - unless params[:snippets].eql? 'true' - .col-sm-10 - = render 'filter', f: f - = hidden_field_tag :project_id, params[:project_id] - = hidden_field_tag :group_id, params[:group_id] - = hidden_field_tag :snippets, params[:snippets] - = hidden_field_tag :scope, params[:scope] - - .results.prepend-top-10 - - if params[:search].present? - = render 'search/results' +- page_title @search_term += render 'search/form' +%hr +- if @search_term + = render 'search/category' + %hr + = render 'search/results' diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index a1121750ca..07672359db 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,21 +1,25 @@ - project = project || @project .git-clone-holder.input-group - .input-group-btn - %button{ | - class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", | - :"data-clone" => project.ssh_url_to_repo, | - :"data-title" => "Add an SSH key to your profile
    to pull or push via SSH", - :"data-html" => "true", - :"data-container" => "body"} - SSH - %button{ | - class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", | - :"data-clone" => project.http_url_to_repo, | - :"data-title" => "Set a password on your account
    to pull or push via #{gitlab_config.protocol.upcase}", - :"data-html" => "true", - :"data-container" => "body"} - = gitlab_config.protocol.upcase - = text_field_tag :project_clone, default_url_to_repo(project), class: "one_click_select form-control", readonly: true + .input-group-addon.git-protocols + .input-group-btn + %button{ | + type: 'button', | + class: "btn btn-sm #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", | + :"data-clone" => project.ssh_url_to_repo, | + :"data-title" => "Add an SSH key to your profile
    to pull or push via SSH", + :"data-html" => "true", + :"data-container" => "body"} + SSH + .input-group-btn + %button{ | + type: 'button', | + class: "btn btn-sm #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", | + :"data-clone" => project.http_url_to_repo, | + :"data-title" => "Set a password on your account
    to pull or push via #{gitlab_config.protocol.upcase}", + :"data-html" => "true", + :"data-container" => "body"} + = gitlab_config.protocol.upcase + = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control input-sm", readonly: true - if project.kind_of?(Project) .input-group-addon .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 30ba361c86..5f51b0d450 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -1,4 +1,4 @@ -#modal-confirm-danger.modal.hide{tabindex: -1} +#modal-confirm-danger.modal{tabindex: -1} .modal-dialog .modal-content .modal-header diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index d07a9e2b92..334db60690 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -3,17 +3,3 @@ = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' - - - if current_user - - if current_controller?(:dashboard) - %li.pull-right - = link_to dashboard_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do - %i.fa.fa-rss - News Feed - - - if current_controller?(:groups) - %li.pull-right - = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do - %i.fa.fa-rss - News Feed -%hr diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml new file mode 100644 index 0000000000..45ec49280d --- /dev/null +++ b/app/views/shared/_field.html.haml @@ -0,0 +1,24 @@ +- name = field[:name] +- title = field[:title] || name.humanize +- value = @service.send(name) +- type = field[:type] +- placeholder = field[:placeholder] +- choices = field[:choices] +- default_choice = field[:default_choice] +- help = field[:help] + +.form-group + = form.label name, title, class: "control-label" + .col-sm-10 + - if type == 'text' + = form.text_field name, class: "form-control", placeholder: placeholder + - elsif type == 'textarea' + = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder + - elsif type == 'checkbox' + = form.check_box name + - elsif type == 'select' + = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } + - elsif type == 'password' + = form.password_field name, value: value, class: 'form-control' + - if help + %span.help-block= help diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index fba69dd0f3..d6a2e177da 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,10 +1,11 @@ .file-content.code{class: user_color_scheme_class} .line-numbers - if blob.data.present? - - blob.data.lines.to_a.size.times do |index| + - blob.data.lines.each_index do |index| - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset - = link_to "#L#{i}", id: "L#{i}", rel: "#L#{i}" do + -# We're not using `link_to` because it is too slow once we get to thousands of lines. + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} %i.fa.fa-link = i :preserve diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml index 722a7f7ce0..6bd61455d2 100644 --- a/app/views/shared/_project.html.haml +++ b/app/views/shared/_project.html.haml @@ -3,8 +3,6 @@ - if avatar .dash-project-avatar = project_icon(project, alt: '', class: 'avatar project-avatar s40') - .dash-project-access-icon - = visibility_level_icon(project.visibility_level) %span.str-truncated %span.namespace-name - if project.namespace @@ -16,6 +14,3 @@ %span.pull-right.light %i.fa.fa-star = project.star_count - - else - %span.arrow - %i.fa.fa-angle-right diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml new file mode 100644 index 0000000000..16a98a7233 --- /dev/null +++ b/app/views/shared/_service_settings.html.haml @@ -0,0 +1,75 @@ +- if @service.errors.any? + #error_explanation + .alert.alert-danger + %ul + - @service.errors.full_messages.each do |msg| + %li= msg + +- if @service.help.present? + .well + = preserve do + = markdown @service.help + +.form-group + = form.label :active, "Active", class: "control-label" + .col-sm-10 + = form.check_box :active + +- if @service.supported_events.length > 1 + .form-group + = form.label :url, "Trigger", class: 'control-label' + .col-sm-10 + - if @service.supported_events.include?("push") + %div + = form.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = form.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + - if @service.supported_events.include?("tag_push") + %div + = form.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = form.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + - if @service.supported_events.include?("note") + %div + = form.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = form.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment + - if @service.supported_events.include?("issue") + %div + = form.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = form.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created + - if @service.supported_events.include?("merge_request") + %div + = form.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = form.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This url will be triggered when a merge request is created + +- @service.fields.each do |field| + - type = field[:type] + + - if type == 'fieldset' + - fields = field[:fields] + - legend = field[:legend] + + %fieldset + %legend= legend + - fields.each do |subfield| + = render 'shared/field', form: form, field: subfield + - else + = render 'shared/field', form: form, field: field diff --git a/app/views/shared/_show_aside.html.haml b/app/views/shared/_show_aside.html.haml new file mode 100644 index 0000000000..3ac9b11b4f --- /dev/null +++ b/app/views/shared/_show_aside.html.haml @@ -0,0 +1,2 @@ += link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml new file mode 100644 index 0000000000..1c6ec198d3 --- /dev/null +++ b/app/views/shared/_visibility_level.html.haml @@ -0,0 +1,14 @@ +.form-group.project-visibility-level-holder + = f.label :visibility_level, class: 'control-label' do + Visibility Level + = link_to "(?)", help_page_path("public_access", "public_access") + .col-sm-10 + - if can_change_visibility_level + = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) + - else + .col-sm-10 + %span.info + = visibility_level_icon(visibility_level) + %strong + = visibility_level_label(visibility_level) + .light= visibility_level_description(visibility_level, form_model) diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml new file mode 100644 index 0000000000..ebe2eb0433 --- /dev/null +++ b/app/views/shared/_visibility_radios.html.haml @@ -0,0 +1,15 @@ +- Gitlab::VisibilityLevel.values.each do |level| + - next if skip_level?(form_model, level) + .radio + - restricted = restricted_visibility_levels.include?(level) + = form.label "#{model_method}_#{level}" do + = form.radio_button model_method, level, checked: (selected_level == level), disabled: restricted + = visibility_level_icon(level) + .option-title + = visibility_level_label(level) + .option-descr + = visibility_level_description(level, form_model) +- unless restricted_visibility_levels.empty? + .col-sm-10 + %span.info + Some visibility level settings have been restricted by the administrator. diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml new file mode 100644 index 0000000000..cba18c1456 --- /dev/null +++ b/app/views/shared/issuable/_context.html.haml @@ -0,0 +1,50 @@ += form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| + %div.prepend-top-20 + .issuable-context-title + %label + Assignee: + - if issuable.assignee + %strong= link_to_member(@project, issuable.assignee, size: 24) + - else + none + .issuable-context-selectbox + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true) + + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Milestone: + - if issuable.milestone + %span.back-to-milestone + = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do + %strong + = icon('clock-o') + = issuable.milestone.title + - else + none + .issuable-context-selectbox + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'}) + = hidden_field_tag :issuable_context + = f.submit class: 'btn hide' + + - if current_user + - subscribed = issuable.subscribed?(current_user) + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Subscription: + %button.btn.btn-block.subscribe-button{:type => 'button'} + = icon('eye') + %span= subscribed ? 'Unsubscribe' : 'Subscribe' + - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' + .subscription-status{data: {status: subscribtion_status}} + .description-block.unsubscribed{class: ( 'hidden' if subscribed )} + You're not receiving notifications from this thread. + .description-block.subscribed{class: ( 'hidden' unless subscribed )} + You're receiving notifications because you're subscribed to this thread. + +:coffeescript + new Subscription("#{toggle_subscription_path(issuable)}") + new IssuableContext() diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/issuable/_filter.html.haml similarity index 55% rename from app/views/shared/_issuable_filter.html.haml rename to app/views/shared/issuable/_filter.html.haml index 83f5a3a801..bcaa48c7a1 100644 --- a/app/views/shared/_issuable_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -3,39 +3,55 @@ %ul.nav.nav-tabs %li{class: ("active" if params[:state] == 'opened')} = link_to page_filter_path(state: 'opened') do - %i.fa.fa-exclamation-circle - Open - %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed') do - %i.fa.fa-check-circle - Closed + = icon('exclamation-circle') + #{state_filters_text_for(:opened, @project)} + + - if defined?(type) && type == :merge_requests + %li{class: ("active" if params[:state] == 'merged')} + = link_to page_filter_path(state: 'merged') do + = icon('check-circle') + #{state_filters_text_for(:merged, @project)} + + %li{class: ("active" if params[:state] == 'closed')} + = link_to page_filter_path(state: 'closed') do + = icon('ban') + #{state_filters_text_for(:closed, @project)} + - else + %li{class: ("active" if params[:state] == 'closed')} + = link_to page_filter_path(state: 'closed') do + = icon('check-circle') + #{state_filters_text_for(:closed, @project)} + %li{class: ("active" if params[:state] == 'all')} = link_to page_filter_path(state: 'all') do - %i.fa.fa-compass - All + = icon('compass') + #{state_filters_text_for(:all, @project)} .issues-details-filters - = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_id, :label_name]), method: :get, class: 'filter-form' do - - if controller.controller_name == 'issues' + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do + - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .check-all-holder = check_box_tag "check_all_issues", nil, false, - class: "check_all_issues left", - disabled: !can?(current_user, :modify_issue, @project) + class: "check_all_issues left" .issues-other-filters .filter-item.inline = users_select_tag(:assignee_id, selected: params[:assignee_id], - placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true) + placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true, current_user: true) .filter-item.inline = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true) + placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true, current_user: true) .filter-item.inline.milestone-filter - = select_tag('milestone_id', projects_milestones_options, class: "select2 trigger-submit", prompt: 'Milestone') + = select_tag('milestone_title', projects_milestones_options, + class: 'select2 trigger-submit', include_blank: true, + data: {placeholder: 'Milestone'}) - if @project .filter-item.inline.labels-filter - = select_tag('label_name', project_labels_options(@project), class: "select2 trigger-submit", prompt: 'Label') + = select_tag('label_name', project_labels_options(@project), + class: 'select2 trigger-submit', include_blank: true, + data: {placeholder: 'Label'}) .pull-right = render 'shared/sort_dropdown' @@ -44,7 +60,7 @@ .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') - = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true) + = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true) = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml new file mode 100644 index 0000000000..09327d645f --- /dev/null +++ b/app/views/shared/issuable/_form.html.haml @@ -0,0 +1,117 @@ +- if issuable.errors.any? + .row + .col-sm-offset-2.col-sm-10 + .alert.alert-danger + - issuable.errors.full_messages.each do |msg| + %span= msg + %br +.form-group + = f.label :title, class: 'control-label' do + %strong= 'Title *' + .col-sm-10 + = f.text_field :title, maxlength: 255, autofocus: true, + class: 'form-control pad js-gfm-input', required: true + + - if issuable.is_a?(MergeRequest) + %p.help-block + - if issuable.work_in_progress? + Remove the WIP prefix from the title to allow this + Work In Progress merge request to be merged when it's ready. + - else + Start the title with [WIP] or WIP: to prevent a + Work In Progress merge request from being merged before it's ready. +.form-group.issuable-description + = f.label :description, 'Description', class: 'control-label' + .col-sm-10 + + = render layout: 'projects/md_preview', locals: { preview_class: "wiki", referenced_users: true } do + = render 'projects/zen', f: f, attr: :description, + classes: 'description form-control' + .col-sm-12.hint + .pull-left + Parsed with + #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. + .pull-right + Attach files by dragging & dropping + or #{link_to 'selecting them', '#', class: 'markdown-selector' }. + + .clearfix + .error-alert + %hr +- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + .form-group + .issue-assignee + = f.label :assignee_id, class: 'control-label' do + %i.fa.fa-user + Assign to + .col-sm-10 + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + placeholder: 'Select a user', class: 'custom-form-control', null_user: true, + selected: issuable.assignee_id, project: @target_project || @project, + first_user: true, current_user: true) +   + = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' + .form-group + .issue-milestone + = f.label :milestone_id, class: 'control-label' do + %i.fa.fa-clock-o + Milestone + .col-sm-10 + - if milestone_options(issuable).present? + = f.select(:milestone_id, milestone_options(issuable), + { include_blank: 'Select milestone' }, { class: 'select2' }) + - else + .prepend-top-10 + %span.light No open milestones available. +   + - if can? current_user, :admin_milestone, issuable.project + = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank + .form-group + = f.label :label_ids, class: 'control-label' do + %i.fa.fa-tag + Labels + .col-sm-10 + - if issuable.project.labels.any? + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2' + - else + .prepend-top-10 + %span.light No labels yet. +   + - if can? current_user, :admin_label, issuable.project + = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank + +- if issuable.is_a?(MergeRequest) + %hr + - if @merge_request.new_record? + .form-group + = f.label :source_branch, class: 'control-label' do + %i.fa.fa-code-fork + Source Branch + .col-sm-10 + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + .form-group + = f.label :target_branch, class: 'control-label' do + %i.fa.fa-code-fork + Target Branch + .col-sm-10 + = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? }) + - if @merge_request.new_record? + %p.help-block + = link_to 'Change branches', mr_change_branches_path(@merge_request) + +.form-actions + - if !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) && !issuable.persisted? + %p + Please review the + %strong #{link_to 'guidelines for contribution', guide_url} + to this repository. + - if issuable.new_record? + = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + - else + = f.submit 'Save changes', class: 'btn btn-save' + - if issuable.new_record? + - cancel_project = issuable.source_project + - else + - cancel_project = issuable.project + = link_to 'Cancel', [cancel_project.namespace.becomes(Namespace), cancel_project, issuable], class: 'btn btn-cancel' diff --git a/app/views/shared/_issuable_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml similarity index 92% rename from app/views/shared/_issuable_search_form.html.haml rename to app/views/shared/issuable/_search_form.html.haml index 639d203dcd..58c3de64b7 100644 --- a/app/views/shared/_issuable_search_form.html.haml +++ b/app/views/shared/issuable/_search_form.html.haml @@ -1,6 +1,6 @@ = form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input' } = hidden_field_tag :state, params['state'] = hidden_field_tag :scope, params['scope'] = hidden_field_tag :assignee_id, params['assignee_id'] diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 30458793fd..d26a99bb14 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,9 +1,5 @@ - unless @snippet.content.empty? - - if gitlab_markdown?(@snippet.file_name) - .file-content.wiki - = preserve do - = markdown(@snippet.data) - - elsif markup?(@snippet.file_name) + - if markup?(@snippet.file_name) .file-content.wiki = render_markup(@snippet.file_name, @snippet.data) - else diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 4e0663ea20..913b674484 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -10,15 +10,15 @@ = f.label :title, class: 'control-label' .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true - = render "shared/snippets/visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true - - .form-group - .file-editor + = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet + + .file-editor + .form-group = f.label :file_name, "File", class: 'control-label' .col-sm-10 .file-holder.snippet .file-title - = f.text_field :file_name, placeholder: "example.rb", class: 'form-control snippet-file-name', required: true + = f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name' .file-content.code %pre#editor= @snippet.content = f.hidden_field :content, class: 'snippet-file-content' @@ -29,7 +29,7 @@ - else = f.submit 'Save', class: "btn-save btn" - - if @snippet.respond_to?(:project) + - if @snippet.project_id = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" diff --git a/app/views/shared/snippets/_visibility_level.html.haml b/app/views/shared/snippets/_visibility_level.html.haml deleted file mode 100644 index 9acff18e45..0000000000 --- a/app/views/shared/snippets/_visibility_level.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -.form-group.project-visibility-level-holder - = f.label :visibility_level, class: 'control-label' do - Visibility Level - = link_to "(?)", help_page_path("public_access", "public_access") - .col-sm-10 - - if can_change_visibility_level - - Gitlab::VisibilityLevel.values.each do |level| - .radio - - restricted = restricted_visibility_levels.include?(level) - = f.radio_button :visibility_level, level, disabled: restricted - = label "#{dom_class(@snippet)}_visibility_level", level do - = visibility_level_icon(level) - .option-title - = visibility_level_label(level) - .option-descr - = snippet_visibility_level_description(level) - - unless restricted_visibility_levels.empty? - .col-sm-10 - %span.info - Some visibility level settings have been restricted by the administrator. - - else - .col-sm-10 - %span.info - = visibility_level_icon(visibility_level) - %strong - = visibility_level_label(visibility_level) - .light= visibility_level_description(visibility_level) diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml index 0df5ade500..0718f74382 100644 --- a/app/views/snippets/current_user_index.html.haml +++ b/app/views/snippets/current_user_index.html.haml @@ -1,39 +1,35 @@ +- page_title "Your Snippets" %h3.page-title Your Snippets .pull-right = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do Add new snippet - = link_to snippets_path, class: "btn btn-grouped" do - Discover snippets %p.light Share code pastes with others out of git repository -%hr -.row - .col-md-3 - %ul.nav.nav-pills.nav-stacked - = nav_tab :scope, nil do - = link_to user_snippets_path(@user) do - All - %span.pull-right - = @user.snippets.count - = nav_tab :scope, 'are_private' do - = link_to user_snippets_path(@user, scope: 'are_private') do - Private - %span.pull-right - = @user.snippets.are_private.count - = nav_tab :scope, 'are_internal' do - = link_to user_snippets_path(@user, scope: 'are_internal') do - Internal - %span.pull-right - = @user.snippets.are_internal.count - = nav_tab :scope, 'are_public' do - = link_to user_snippets_path(@user, scope: 'are_public') do - Public - %span.pull-right - = @user.snippets.are_public.count +%ul.nav.nav-tabs + = nav_tab :scope, nil do + = link_to user_snippets_path(@user) do + All + %span.badge + = @user.snippets.count + = nav_tab :scope, 'are_private' do + = link_to user_snippets_path(@user, scope: 'are_private') do + Private + %span.badge + = @user.snippets.are_private.count + = nav_tab :scope, 'are_internal' do + = link_to user_snippets_path(@user, scope: 'are_internal') do + Internal + %span.badge + = @user.snippets.are_internal.count + = nav_tab :scope, 'are_public' do + = link_to user_snippets_path(@user, scope: 'are_public') do + Public + %span.badge + = @user.snippets.are_public.count - .col-md-9.my-snippets - = render 'snippets' +.my-snippets + = render 'snippets' diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 7042d07d5e..1a38003566 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1,4 +1,5 @@ +- page_title "Edit", @snippet.title, "Snippets" %h3.page-title Edit snippet %hr -= render "shared/snippets/form", url: snippet_path(@snippet) += render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 5cd8ae26cf..e9bb6a908d 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,8 +1,8 @@ +- page_title "Public Snippets" %h3.page-title Public snippets .pull-right - - if current_user = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do Add new snippet diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 694d705831..a74d5e792a 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1,4 +1,5 @@ +- page_title "New Snippet" %h3.page-title New snippet %hr -= render "shared/snippets/form", url: snippets_path(@snippet) += render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 5204fb9a90..089e812291 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,3 +1,4 @@ +- page_title @snippet.title, "Snippets" %h3.page-title = @snippet.title @@ -35,7 +36,7 @@ = @snippet.file_name .file-actions .btn-group - - if can?(current_user, :modify_personal_snippet, @snippet) + - if can?(current_user, :update_personal_snippet, @snippet) = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet' = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - if can?(current_user, :admin_personal_snippet, @snippet) diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml index df524cd18b..23700eb39d 100644 --- a/app/views/snippets/user_index.html.haml +++ b/app/views/snippets/user_index.html.haml @@ -1,3 +1,4 @@ +- page_title "Snippets", @user.name %h3.page-title = image_tag avatar_icon(@user.email), class: "avatar s24" = @user.name diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index 8fe30b2363..50232dc718 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Activity feed for #{@user.name}" + xml.title "#{@user.name} activity" xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" xml.link href: user_url(@user), rel: "alternate", type: "text/html" - xml.id projects_url + xml.id user_url(@user) xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 9dd8cb0738..64b7f25ad3 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,16 +1,33 @@ +- page_title @user.name +- header_title @user.name, user_path(@user) + += content_for :meta_tags do + = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") + += render 'shared/show_aside' + .row - = link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left %section.col-md-8 .header-with-avatar - = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: '' + = link_to avatar_icon(@user.email, 400), target: '_blank' do + = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: '' %h3 = @user.name - if @user == current_user - .pull-right + .pull-right.hidden-xs = link_to profile_path, class: 'btn btn-sm' do %i.fa.fa-pencil-square-o Edit Profile settings + - elsif current_user + .pull-right + %span.dropdown + %a.light.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} + = icon('exclamation-circle') + %ul.dropdown-menu.dropdown-menu-right + %li + = link_to new_abuse_report_path(user_id: @user.id) do + Report abuse + .username @#{@user.username} .description diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 8b50f42398..2d44d8d4dc 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -19,7 +19,7 @@ class IrkerWorker branch = "\x0305#{branch}\x0f" end - # Firsts messages are for branch creation/deletion + # First messages are for branch creation/deletion send_branch_updates push_data, project, repo_name, committer, branch # Next messages are for commits @@ -34,7 +34,7 @@ class IrkerWorker def init_perform(set, chans, colors) @colors = colors @channels = chans - start_connection set['server_ip'], set['server_port'] + start_connection set['server_host'], set['server_port'] end def start_connection(irker_server, irker_port) @@ -137,8 +137,7 @@ class IrkerWorker end def commit_from_id(project, id) - commit = Gitlab::Git::Commit.find(project.repository, id) - Commit.new(commit) + project.commit(id) end def files_count(commit) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 33d8cc8861..994b8e8ed3 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -45,7 +45,7 @@ class PostReceive def utf8_encode_changes(changes) changes = changes.dup - + changes.force_encoding("UTF-8") return changes if changes.valid_encoding? diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb new file mode 100644 index 0000000000..55cb6af232 --- /dev/null +++ b/app/workers/project_cache_worker.rb @@ -0,0 +1,15 @@ +class ProjectCacheWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(project_id) + project = Project.find(project_id) + project.update_repository_size + project.update_commit_count + + if project.repository.root_ref + project.repository.build_cache + end + end +end diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb index 73085c046b..fb87896528 100644 --- a/app/workers/project_web_hook_worker.rb +++ b/app/workers/project_web_hook_worker.rb @@ -3,8 +3,8 @@ class ProjectWebHookWorker sidekiq_options queue: :project_web_hook - def perform(hook_id, data) + def perform(hook_id, data, hook_name) data = data.with_indifferent_access - WebHook.find(hook_id).execute(data) + WebHook.find(hook_id).execute(data, hook_name) end end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index e6a50afedb..94832872d1 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -28,7 +28,7 @@ class RepositoryImportWorker project.import_finish project.save project.satellite.create unless project.satellite.exists? - project.update_repository_size + ProjectCacheWorker.perform_async(project.id) Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' end end diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb index 3ebc62b7e7..a122c27476 100644 --- a/app/workers/system_hook_worker.rb +++ b/app/workers/system_hook_worker.rb @@ -3,7 +3,7 @@ class SystemHookWorker sidekiq_options queue: :system_hook - def perform(hook_id, data) - SystemHook.find(hook_id).execute data + def perform(hook_id, data, hook_name) + SystemHook.find(hook_id).execute(data, hook_name) end end diff --git a/bin/guard b/bin/guard deleted file mode 100755 index 0c1a532bd0..0000000000 --- a/bin/guard +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env ruby -# -# This file was generated by Bundler. -# -# The application 'guard' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require 'pathname' -ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require 'rubygems' -require 'bundler/setup' - -load Gem.bin_path('guard', 'guard') diff --git a/bin/rake b/bin/rake index 8017a0271d..0fb4e07e13 100755 --- a/bin/rake +++ b/bin/rake @@ -3,6 +3,5 @@ begin load File.expand_path("../spring", __FILE__) rescue LoadError end -require_relative '../config/boot' -require 'rake' -Rake.application.run +require 'bundler/setup' +load Gem.bin_path('rake', 'rake') diff --git a/bin/spring b/bin/spring index 253ec37c34..7b45d374fc 100755 --- a/bin/spring +++ b/bin/spring @@ -1,17 +1,14 @@ #!/usr/bin/env ruby -# This file loads spring without using Bundler, in order to be fast -# It gets overwritten when you run the `spring binstub` command +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) require "rubygems" require "bundler" - if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) - ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) - ENV["GEM_HOME"] = "" - Gem.paths = ENV - + if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) + Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } gem "spring", match[1] require "spring/binstub" end diff --git a/config.ru b/config.ru index e90863a5c2..a2525c8136 100644 --- a/config.ru +++ b/config.ru @@ -2,11 +2,14 @@ if defined?(Unicorn) require 'unicorn' - # Unicorn self-process killer - require 'unicorn/worker_killer' - # Max memory size (RSS) per worker - use Unicorn::WorkerKiller::Oom, (200 * (1 << 20)), (250 * (1 << 20)) + if ENV['RAILS_ENV'] == 'production' || ENV['RAILS_ENV'] == 'staging' + # Unicorn self-process killer + require 'unicorn/worker_killer' + + # Max memory size (RSS) per worker + use Unicorn::WorkerKiller::Oom, (200 * (1 << 20)), (250 * (1 << 20)) + end end require ::File.expand_path('../config/environment', __FILE__) diff --git a/config/application.rb b/config/application.rb index fa399533e5..a96e22211e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,7 +31,7 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters.push(:password, :password_confirmation, :private_token) + config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt) # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true @@ -96,6 +96,7 @@ module Gitlab end redis_config_hash[:namespace] = 'cache:gitlab' + redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash # This is needed for gitlab-shell diff --git a/config/aws.yml.example b/config/aws.yml.example index 29d029b078..bb10c3cec7 100644 --- a/config/aws.yml.example +++ b/config/aws.yml.example @@ -1,5 +1,8 @@ # See https://github.com/jnicklas/carrierwave#using-amazon-s3 # for more options +# If you change this file in a Merge Request, please also create +# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +# production: access_key_id: AKIA1111111111111UA secret_access_key: secret diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index ba40671b16..56770335dd 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -4,14 +4,21 @@ # ########################### NOTE ##################################### # This file should not receive new settings. All configuration options # -# are being moved to ApplicationSetting model! # +# that do not require application restart are being moved to # +# ApplicationSetting model! # +# If you change this file in a Merge Request, please also create # +# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests # ######################################################################## # +# # How to use: # 1. Copy file as gitlab.yml # 2. Update gitlab -> host with your fully qualified domain name # 3. Update gitlab -> email_from # 4. If you installed Git from source, change git -> bin_path to /usr/local/bin/git +# IMPORTANT: If Git was installed in a different location use that instead. +# You can check with `which git`. If a wrong path of Git is specified, it will +# result in various issues such as failures of GitLab CI builds. # 5. Review this configuration file for other settings you may want to adjust production: &base @@ -55,12 +62,13 @@ production: &base # default_can_create_group: false # default: true # username_changing_enabled: false # default: true - User can change her username/namespace - ## Default theme - ## BASIC = 1 - ## MARS = 2 - ## MODERN = 3 - ## GRAY = 4 - ## COLOR = 5 + ## Default theme ID + ## 1 - Graphite + ## 2 - Charcoal + ## 3 - Green + ## 4 - Gray + ## 5 - Violet + ## 6 - Blue # default_theme: 2 # default: 2 ## Automatic issue closing @@ -76,7 +84,6 @@ production: &base merge_requests: true wiki: true snippets: false - visibility_level: "private" # can be "private" | "internal" | "public" ## Webhook settings # Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10) @@ -147,7 +154,7 @@ production: &base allow_username_or_email_login: false # To maintain tight control over the number of active users on your GitLab installation, - # enable this setting to keep new users blocked until they have been cleared by the admin + # enable this setting to keep new users blocked until they have been cleared by the admin # (default: false). block_auto_created_users: false @@ -180,12 +187,19 @@ production: &base # Allow login via Twitter, Google, etc. using OmniAuth providers enabled: false + # Uncomment this to automatically sign in with a specific omniauth provider's without + # showing GitLab's sign-in page (default: show the GitLab sign-in page) + # auto_sign_in_with_provider: saml + # CAUTION! # This allows users to login without having a user account first (default: false). # User accounts will be created automatically when authentication was successful. allow_single_sign_on: false # Locks down those users until they have been cleared by the admin (default: true). block_auto_created_users: true + # Look up new users in LDAP servers. If a match is found (same uid), automatically + # link the omniauth identity with the LDAP account. (default: false) + auto_link_ldap_user: false ## Auth providers # Uncomment the following lines and fill in the data of the auth provider you want to use @@ -195,19 +209,37 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: - # - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', + # - { name: 'google_oauth2', + # label: 'Google', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', app_id: 'YOUR_APP_ID', - # app_secret: 'YOUR_APP_SECRET'} - # - { name: 'github', app_id: 'YOUR_APP_ID', + # - { name: 'twitter', + # app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET' } + # - { name: 'github', + # label: 'GitHub', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'user:email' } } - # - { name: 'gitlab', app_id: 'YOUR_APP_ID', + # - { name: 'gitlab', + # label: 'GitLab.com', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'api' } } - # - { name: 'bitbucket', app_id: 'YOUR_APP_ID', - # app_secret: 'YOUR_APP_SECRET'} + # - { name: 'bitbucket', + # app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET' } + # - { name: 'saml', + # label: 'Our SAML Provider', + # args: { + # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + # idp_sso_target_url: 'https://login.example.com/idp', + # issuer: 'https://gitlab.example.com', + # name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + # } } + @@ -224,6 +256,7 @@ production: &base ## Backup settings backup: path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/) + # archive_permissions: 0640 # Permissions for the resulting backup.tar file (default: 0600) # keep_time: 604800 # default: 0 (forever) (in seconds) # upload: # # Fog storage connection settings, see http://fog.io/storage/ . @@ -234,6 +267,9 @@ production: &base # aws_secret_access_key: 'secret123' # # The remote 'directory' to store your backups. For S3, this would be the bucket name. # remote_directory: 'my.s3.bucket' + # # Use multipart uploads when file size reaches 100MB, see + # # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html + # multipart_chunk_size: 104857600 ## GitLab Shell settings gitlab_shell: @@ -243,6 +279,10 @@ production: &base repos_path: /home/git/repositories/ hooks_path: /home/git/gitlab-shell/hooks/ + # File that contains the secret key for verifying access for gitlab-shell. + # Default is '.gitlab_shell_secret' relative to Rails.root (i.e. root of the GitLab app). + # secret_file: /home/git/gitlab/.gitlab_shell_secret + # Git over HTTP upload_pack: true receive_pack: true @@ -308,6 +348,8 @@ test: # user: YOUR_USERNAME satellites: path: tmp/tests/gitlab-satellites/ + backup: + path: tmp/tests/backups gitlab_shell: path: tmp/tests/gitlab-shell/ repos_path: tmp/tests/repositories/ diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0abd34fc3e..3a2b7d341d 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -9,6 +9,15 @@ class Settings < Settingslogic gitlab.port.to_i == (gitlab.https ? 443 : 80) end + # get host without www, thanks to http://stackoverflow.com/a/6674363/1233435 + def get_host_without_www(url) + url = URI.encode(url) + uri = URI.parse(url) + uri = URI.parse("http://#{url}") if uri.scheme.nil? + host = uri.host.downcase + host.start_with?('www.') ? host[4..-1] : host + end + private def build_gitlab_shell_ssh_path_prefix @@ -23,14 +32,12 @@ class Settings < Settingslogic end end + def build_base_gitlab_url + base_gitlab_url.join('') + end + def build_gitlab_url - custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}" - [ gitlab.protocol, - "://", - gitlab.host, - custom_port, - gitlab.relative_url_root - ].join('') + (base_gitlab_url + [gitlab.relative_url_root]).join('') end # check that values in `current` (string or integer) is a contant in `modul`. @@ -55,6 +62,17 @@ class Settings < Settingslogic end value end + + private + + def base_gitlab_url + custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}" + [ gitlab.protocol, + "://", + gitlab.host, + custom_port + ] + end end end @@ -87,6 +105,11 @@ end Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? +Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? +Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? +Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? +Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? + Settings.omniauth['providers'] ||= [] Settings['issues_tracker'] ||= {} @@ -98,7 +121,7 @@ Settings['gitlab'] ||= Settingslogic.new({}) Settings.gitlab['default_projects_limit'] ||= 10 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? -Settings.gitlab['default_theme'] = Gitlab::Theme::MARS if Settings.gitlab['default_theme'].nil? +Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? @@ -109,6 +132,7 @@ Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].ni Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_display_name'] ||= "GitLab" Settings.gitlab['email_reply_to'] ||= "noreply@#{Settings.gitlab.host}" +Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url) Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' Settings.gitlab['user_home'] ||= begin @@ -126,12 +150,14 @@ Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 +Settings.gitlab['session_expire_delay'] ||= 10080 Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root) +Settings.gitlab['restricted_signup_domains'] ||= [] # # Gravatar @@ -140,6 +166,7 @@ Settings['gravatar'] ||= Settingslogic.new({}) Settings.gravatar['enabled'] = true if Settings.gravatar['enabled'].nil? Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' +Settings.gravatar['host'] = Settings.get_host_without_www(Settings.gravatar['plain_url']) # # GitLab Shell @@ -147,6 +174,7 @@ Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}? Settings['gitlab_shell'] ||= Settingslogic.new({}) Settings.gitlab_shell['path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/' Settings.gitlab_shell['hooks_path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/hooks/' +Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret') Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil? Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil? Settings.gitlab_shell['repos_path'] ||= Settings.gitlab['user_home'] + '/repositories/' @@ -162,11 +190,13 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) +Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) # Convert upload connection settings to use symbol keys, to make Fog happy if Settings.backup['upload']['connection'] Settings.backup['upload']['connection'] = Hash[Settings.backup['upload']['connection'].map { |k, v| [k.to_sym, v] }] end +Settings.backup['upload']['multipart_chunk_size'] ||= 104857600 # # Git diff --git a/config/initializers/6_rack_profiler.rb b/config/initializers/6_rack_profiler.rb index b634028756..1d958904e8 100644 --- a/config/initializers/6_rack_profiler.rb +++ b/config/initializers/6_rack_profiler.rb @@ -1,8 +1,10 @@ -if Rails.env == 'development' +if Rails.env.development? require 'rack-mini-profiler' # initialization is skipped so trigger it Rack::MiniProfilerRails.initialize!(Rails.application) + Rack::MiniProfiler.config.position = 'right' - Rack::MiniProfiler.config.start_hidden = true + Rack::MiniProfiler.config.start_hidden = false + Rack::MiniProfiler.config.skip_paths << '/teaspoon' end diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index 8f6c567310..70ed10e827 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -10,3 +10,19 @@ if Gitlab::LDAP::Config.enabled? alias_method server['provider_name'], :ldap end end + +OmniAuth.config.full_host = Settings.gitlab['base_url'] +OmniAuth.config.allowed_request_methods = [:post] +#In case of auto sign-in, the GET method is used (users don't get to click on a button) +OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? +OmniAuth.config.before_request_phase do |env| + OmniAuth::RequestForgeryProtection.new(env).call +end + +if Gitlab.config.omniauth.enabled + Gitlab.config.omniauth.providers.each do |provider| + if provider['name'] == 'kerberos' + require 'omniauth-kerberos' + end + end +end diff --git a/config/initializers/8_default_url_options.rb b/config/initializers/8_default_url_options.rb new file mode 100644 index 0000000000..8fd27b1d88 --- /dev/null +++ b/config/initializers/8_default_url_options.rb @@ -0,0 +1,11 @@ +default_url_options = { + host: Gitlab.config.gitlab.host, + protocol: Gitlab.config.gitlab.protocol, + script_name: Gitlab.config.gitlab.relative_url_root +} + +unless Gitlab.config.gitlab_on_standard_port? + default_url_options[:port] = Gitlab.config.gitlab.port +end + +Rails.application.routes.default_url_options = default_url_options diff --git a/config/initializers/attr_encrypted_no_db_connection.rb b/config/initializers/attr_encrypted_no_db_connection.rb new file mode 100644 index 0000000000..c668864089 --- /dev/null +++ b/config/initializers/attr_encrypted_no_db_connection.rb @@ -0,0 +1,20 @@ +module AttrEncrypted + module Adapters + module ActiveRecord + def attribute_instance_methods_as_symbols_with_no_db_connection + # Use with_connection so the connection doesn't stay pinned to the thread. + connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false + + if connected + # Call version from AttrEncrypted::Adapters::ActiveRecord + attribute_instance_methods_as_symbols_without_no_db_connection + else + # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord + AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call + end + end + + alias_method_chain :attribute_instance_methods_as_symbols, :no_db_connection + end + end +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 9dce495106..091548348b 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,14 +1,14 @@ # Use this hook to configure devise mailer, warden hooks and so forth. The first # four configuration values can also be set straight in your models. Devise.setup do |config| + config.warden do |manager| + manager.default_strategies(scope: :user).unshift :two_factor_authenticatable + manager.default_strategies(scope: :user).unshift :two_factor_backupable + end + # ==> Mailer Configuration - # Configure the e-mail address which will be shown in Devise::Mailer, - # note that it will be overwritten if you use your own mailer class with default "from" parameter. - config.mailer_sender = "GitLab <#{Gitlab.config.gitlab.email_from}>" - - # Configure the class responsible to send e-mails. - # config.mailer = "Devise::Mailer" + config.mailer = "DeviseMailer" # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index d422acb31d..6139ddbe6c 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -6,7 +6,8 @@ Doorkeeper.configure do # This block will be called to check whether the resource owner is authenticated or not. resource_owner_authenticator do # Put your resource owner authentication logic here. - # Example implementation: + # Ensure user is redirected to redirect_uri after login + session[:user_return_to] = request.fullpath current_user || redirect_to(new_user_session_url) end diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb index e7c9f0ba7c..751fccead0 100644 --- a/config/initializers/gitlab_shell_secret_token.rb +++ b/config/initializers/gitlab_shell_secret_token.rb @@ -5,8 +5,7 @@ require 'securerandom' # Your secret key for verifying the gitlab_shell. -secret_file = Rails.root.join('.gitlab_shell_secret') -gitlab_shell_symlink = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret') +secret_file = Gitlab.config.gitlab_shell.secret_file unless File.exist? secret_file # Generate a new token of 16 random hexadecimal characters and store it in secret_file. @@ -14,6 +13,7 @@ unless File.exist? secret_file File.write(secret_file, token) end -if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(gitlab_shell_symlink) - FileUtils.symlink(secret_file, gitlab_shell_symlink) +link_path = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret') +if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(link_path) + FileUtils.symlink(secret_file, link_path) end diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example index 332865d288..b1bbcca1d6 100644 --- a/config/initializers/rack_attack.rb.example +++ b/config/initializers/rack_attack.rb.example @@ -1,6 +1,7 @@ # 1. Rename this file to rack_attack.rb # 2. Review the paths_to_be_protected and add any other path you need protecting # +# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests paths_to_be_protected = [ "#{Rails.application.config.relative_url_root}/users/password", diff --git a/config/initializers/redis-store-fix-expiry.rb b/config/initializers/redis-store-fix-expiry.rb deleted file mode 100644 index fce0a13533..0000000000 --- a/config/initializers/redis-store-fix-expiry.rb +++ /dev/null @@ -1,44 +0,0 @@ -# Monkey-patch Redis::Store to make 'setex' and 'expire' work with namespacing - -module Gitlab - class Redis - class Store - module Namespace - # Redis::Store#setex in redis-store 1.1.4 does not respect namespaces; - # this new method does. - def setex(key, expires_in, value, options=nil) - namespace(key) { |key| super(key, expires_in, value) } - end - - # Redis::Store#expire in redis-store 1.1.4 does not respect namespaces; - # this new method does. - def expire(key, expires_in) - namespace(key) { |key| super(key, expires_in) } - end - - private - - # Our new definitions of #setex and #expire above assume that the - # #namespace method exists. Because we cannot be sure of that, we - # re-implement the #namespace method from Redis::Store::Namespace so - # that it is available for all Redis::Store instances, whether they use - # namespacing or not. - # - # Based on lib/redis/store/namespace.rb L49-51 (redis-store 1.1.4) - def namespace(key) - if @namespace - yield interpolate(key) - else - # This Redis::Store instance does not use a namespace so we should - # just pass through the key. - yield key - end - end - end - end - end -end - -Redis::Store.class_eval do - include Gitlab::Redis::Store::Namespace -end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index b2d59f1c4b..6d274cd95a 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,11 +1,15 @@ # Be sure to restart your server when you modify this file. +require 'gitlab/current_settings' +include Gitlab::CurrentSettings +Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay + Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store key: '_gitlab_session', secure: Gitlab.config.gitlab.https, httponly: true, - expire_after: 1.week, + expire_after: Settings.gitlab['session_expire_delay'] * 60, path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root ) diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index f0fe2fdfa4..25ec247a09 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -5,6 +5,7 @@ # # For full list of options and their values see http://api.rubyonrails.org/classes/ActionMailer/Base.html # +# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests if Rails.env.production? Gitlab::Application.config.action_mailer.delivery_method = :smtp diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index c5b6b75e7f..a4032a2142 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -31,7 +31,7 @@ en: messages: # Common error messages invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' - invalid_redirect_uri: 'The redirect uri included is not valid.' + invalid_redirect_uri: 'The redirect URI included is not valid.' unauthorized_client: 'The client is not authorized to perform this request using this method.' access_denied: 'The resource owner or authorization server denied the request.' invalid_scope: 'The requested scope is invalid, unknown, or malformed.' @@ -63,11 +63,11 @@ en: flash: applications: create: - notice: 'Application created.' + notice: 'The application was created successfully.' destroy: - notice: 'Application deleted.' + notice: 'The application was deleted successfully.' update: - notice: 'Application updated.' + notice: 'The application was updated successfully.' authorized_applications: destroy: - notice: 'Application revoked.' + notice: 'The application was revoked access.' diff --git a/config/resque.yml.example b/config/resque.yml.example index 347f3599b2..d98f43f71b 100644 --- a/config/resque.yml.example +++ b/config/resque.yml.example @@ -1,3 +1,6 @@ +# If you change this file in a Merge Request, please also create +# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +# development: redis://localhost:6379 test: redis://localhost:6379 production: unix:/var/run/redis/redis.sock diff --git a/config/routes.rb b/config/routes.rb index 744a99fede..1166a4b3eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,7 +51,8 @@ Gitlab::Application.routes.draw do get 'raw' end end - get '/s/:username' => 'snippets#user_index', as: :user_snippets, constraints: { username: /.*/ } + + get '/s/:username' => 'snippets#index', as: :user_snippets, constraints: { username: /.*/ } # # Invites @@ -64,6 +65,9 @@ Gitlab::Application.routes.draw do end end + # Spam reports + resources :abuse_reports, only: [:new, :create] + # # Import # @@ -148,14 +152,23 @@ Gitlab::Application.routes.draw do namespace :admin do resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do resources :keys, only: [:show, :destroy] + resources :identities, only: [:index, :edit, :update, :destroy] + member do + get :projects + get :keys + get :groups put :team_update put :block put :unblock + put :unlock + put :confirm + patch :disable_two_factor delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' end end + resources :abuse_reports, only: [:index, :destroy] resources :applications resources :groups, constraints: { id: /[^\/]+/ } do @@ -164,7 +177,7 @@ Gitlab::Application.routes.draw do end end - resources :deploy_keys, only: [:index, :show, :new, :create, :destroy] + resources :deploy_keys, only: [:index, :new, :create, :destroy] resources :hooks, only: [:index, :create, :destroy] do get :test @@ -201,8 +214,7 @@ Gitlab::Application.routes.draw do # resource :profile, only: [:show, :update] do member do - get :history - get :design + get :audit_log get :applications put :reset_private_token @@ -221,9 +233,15 @@ Gitlab::Application.routes.draw do put :reset end end + resource :preferences, only: [:show, :update] resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] + resource :two_factor_auth, only: [:new, :create, :destroy] do + member do + post :codes + end + end end end @@ -234,7 +252,7 @@ Gitlab::Application.routes.draw do constraints: { username: /.*/ } get '/u/:username' => 'users#show', as: :user, - constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } + constraints: { username: /[a-zA-Z.0-9_\-]+(? 'omniauth_callbacks#omniauth_error', as: :omniauth_error end - root to: "dashboard#show" + root to: "root#show" # # Project Area # resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do - resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+/ }, except: + resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(? 'refs#logs_tree', as: :logs_file, constraints: { + get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: { id: Gitlab::Regex.git_reference_regex, path: /.*/ } @@ -443,6 +462,7 @@ Gitlab::Application.routes.draw do resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do member do get :diffs + get :commits post :automerge get :automerge_check get :ci_status @@ -466,7 +486,7 @@ Gitlab::Application.routes.draw do end end - resources :milestones, except: [:destroy], constraints: { id: /\d+/ } do + resources :milestones, constraints: { id: /\d+/ } do member do put :sort_issues put :sort_merge_requests diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index 86a5512e76..b937b09278 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -8,6 +8,9 @@ # See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete # documentation. +# Note: If you change this file in a Merge Request, please also create a +# Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +# # WARNING: See config/application.rb under "Relative url support" for the list of # other files that need to be changed for relative url support # diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index ae4c0550a4..8f71198e47 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -11,9 +11,42 @@ Sidekiq::Testing.inline! do 'https://github.com/twitter/flight.git', 'https://github.com/twitter/typeahead.js.git', 'https://github.com/h5bp/html5-boilerplate.git', + 'https://github.com/google/material-design-lite.git', + 'https://github.com/jlevy/the-art-of-command-line.git', + 'https://github.com/FreeCodeCamp/freecodecamp.git', + 'https://github.com/google/deepdream.git', + 'https://github.com/jtleek/datasharing.git', + 'https://github.com/WebAssembly/design.git', + 'https://github.com/airbnb/javascript.git', + 'https://github.com/tessalt/echo-chamber-js.git', + 'https://github.com/atom/atom.git', + 'https://github.com/ipselon/react-ui-builder.git', + 'https://github.com/mattermost/platform.git', + 'https://github.com/purifycss/purifycss.git', + 'https://github.com/facebook/nuclide.git', + 'https://github.com/wbkd/awesome-d3.git', + 'https://github.com/kilimchoi/engineering-blogs.git', + 'https://github.com/gilbarbara/logos.git', + 'https://github.com/gaearon/redux.git', + 'https://github.com/awslabs/s2n.git', + 'https://github.com/arkency/reactjs_koans.git', + 'https://github.com/twbs/bootstrap.git', + 'https://github.com/chjj/ttystudio.git', + 'https://github.com/DrBoolean/mostly-adequate-guide.git', + 'https://github.com/octocat/Spoon-Knife.git', + 'https://github.com/opencontainers/runc.git', + 'https://github.com/googlesamples/android-topeka.git' ] - project_urls.each_with_index do |url, i| + # You can specify how many projects you need during seed execution + size = if ENV['SIZE'].present? + ENV['SIZE'].to_i + else + 8 + end + + + project_urls.first(size).each_with_index do |url, i| group_path, project_path = url.split('/')[-2..-1] group = Group.find_by(path: group_path) @@ -23,7 +56,7 @@ Sidekiq::Testing.inline! do name: group_path.titleize, path: group_path ) - group.description = Faker::Lorem.sentence + group.description = FFaker::Lorem.sentence group.save group.add_owner(User.first) @@ -35,7 +68,7 @@ Sidekiq::Testing.inline! do import_url: url, namespace_id: group.id, name: project_path.titleize, - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, visibility_level: Gitlab::VisibilityLevel.values.sample } diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb index 24952a1f66..378354efd5 100644 --- a/db/fixtures/development/05_users.rb +++ b/db/fixtures/development/05_users.rb @@ -2,9 +2,9 @@ Gitlab::Seeder.quiet do (2..20).each do |i| begin User.create!( - username: Faker::Internet.user_name, - name: Faker::Name.name, - email: Faker::Internet.email, + username: FFaker::Internet.user_name, + name: FFaker::Name.name, + email: FFaker::Internet.email, confirmed_at: DateTime.now, password: '12345678' ) diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb index 2296821e52..a43116829d 100644 --- a/db/fixtures/development/07_milestones.rb +++ b/db/fixtures/development/07_milestones.rb @@ -3,7 +3,7 @@ Gitlab::Seeder.quiet do (1..5).each do |i| milestone_params = { title: "v#{i}.0", - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, state: ['opened', 'closed'].sample, } diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb index e8b01b46d2..c636e96381 100644 --- a/db/fixtures/development/09_issues.rb +++ b/db/fixtures/development/09_issues.rb @@ -2,8 +2,8 @@ Gitlab::Seeder.quiet do Project.all.each do |project| (1..10).each do |i| issue_params = { - title: Faker::Lorem.sentence(6), - description: Faker::Lorem.sentence, + title: FFaker::Lorem.sentence(6), + description: FFaker::Lorem.sentence, state: ['opened', 'closed'].sample, milestone: project.milestones.sample, assignee: project.team.users.sample diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index f9b2fd8b05..0825776ffa 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -10,8 +10,8 @@ Gitlab::Seeder.quiet do params = { source_branch: source_branch, target_branch: target_branch, - title: Faker::Lorem.sentence(6), - description: Faker::Lorem.sentences(3).join(" "), + title: FFaker::Lorem.sentence(6), + description: FFaker::Lorem.sentences(3).join(" "), milestone: project.milestones.sample, assignee: project.team.users.sample } diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb index b3a6f39c7d..3bd4b442ad 100644 --- a/db/fixtures/development/12_snippets.rb +++ b/db/fixtures/development/12_snippets.rb @@ -28,8 +28,8 @@ eos PersonalSnippet.seed(:id, [{ id: i, author_id: user.id, - title: Faker::Lorem.sentence(3), - file_name: Faker::Internet.domain_word + '.rb', + title: FFaker::Lorem.sentence(3), + file_name: FFaker::Internet.domain_word + '.rb', visibility_level: Gitlab::VisibilityLevel.values.sample, content: content, }]) diff --git a/db/fixtures/development/13_comments.rb b/db/fixtures/development/13_comments.rb index d37be53c7b..566c070563 100644 --- a/db/fixtures/development/13_comments.rb +++ b/db/fixtures/development/13_comments.rb @@ -6,7 +6,7 @@ Gitlab::Seeder.quiet do note_params = { noteable_type: 'Issue', noteable_id: issue.id, - note: Faker::Lorem.sentence, + note: FFaker::Lorem.sentence, } Notes::CreateService.new(project, user, note_params).execute @@ -21,7 +21,7 @@ Gitlab::Seeder.quiet do note_params = { noteable_type: 'MergeRequest', noteable_id: mr.id, - note: Faker::Lorem.sentence, + note: FFaker::Lorem.sentence, } Notes::CreateService.new(project, user, note_params).execute diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index 8b560ee09e..1c8740f6ba 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -12,7 +12,7 @@ admin = User.create( username: 'root', password: password, password_expires_at: expire_time, - theme_id: Gitlab::Theme::MARS + theme_id: Gitlab::Themes::APPLICATION_DEFAULT ) diff --git a/db/migrate/20141118150935_add_audit_event.rb b/db/migrate/20141118150935_add_audit_event.rb new file mode 100644 index 0000000000..07383c6bbc --- /dev/null +++ b/db/migrate/20141118150935_add_audit_event.rb @@ -0,0 +1,22 @@ +class AddAuditEvent < ActiveRecord::Migration + def change + create_table :audit_events do |t| + t.integer :author_id, null: false + t.string :type, null: false + + # "Namespace" where the change occurs + # eg. On a project, group or user + t.integer :entity_id, null: false + t.string :entity_type, null: false + + # Details for the event + t.text :details + + t.timestamps + end + + add_index :audit_events, :author_id + add_index :audit_events, :type + add_index :audit_events, [:entity_id, :entity_type] + end +end diff --git a/db/migrate/20150310194358_add_version_check_to_application_settings.rb b/db/migrate/20150310194358_add_version_check_to_application_settings.rb new file mode 100644 index 0000000000..e9d42c1e74 --- /dev/null +++ b/db/migrate/20150310194358_add_version_check_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddVersionCheckToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :version_check_enabled, :boolean, default: true + end +end diff --git a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb new file mode 100644 index 0000000000..11b026ee8f --- /dev/null +++ b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb @@ -0,0 +1,8 @@ +class AddDeviseTwoFactorToUsers < ActiveRecord::Migration + def change + add_column :users, :encrypted_otp_secret, :string + add_column :users, :encrypted_otp_secret_iv, :string + add_column :users, :encrypted_otp_secret_salt, :string + add_column :users, :otp_required_for_login, :boolean + end +end diff --git a/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb new file mode 100644 index 0000000000..913958db7c --- /dev/null +++ b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb @@ -0,0 +1,5 @@ +class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration + def change + add_column :users, :otp_backup_codes, :text + end +end diff --git a/db/migrate/20150406133311_add_invite_data_to_member.rb b/db/migrate/20150406133311_add_invite_data_to_member.rb index 3452fd45c4..5d3e856ddc 100644 --- a/db/migrate/20150406133311_add_invite_data_to_member.rb +++ b/db/migrate/20150406133311_add_invite_data_to_member.rb @@ -1,5 +1,5 @@ class AddInviteDataToMember < ActiveRecord::Migration - def change + def up add_column :members, :created_by_id, :integer add_column :members, :invite_email, :string add_column :members, :invite_token, :string @@ -9,4 +9,15 @@ class AddInviteDataToMember < ActiveRecord::Migration add_index :members, :invite_token, unique: true end + + def down + remove_index :members, :invite_token + + change_column :members, :user_id, :integer, null: false + + remove_column :members, :invite_accepted_at + remove_column :members, :invite_token + remove_column :members, :invite_email + remove_column :members, :created_by_id + end end diff --git a/db/migrate/20150417122318_remove_import_data_from_project.rb b/db/migrate/20150417122318_remove_import_data_from_project.rb index c275b49d22..46cf63593c 100644 --- a/db/migrate/20150417122318_remove_import_data_from_project.rb +++ b/db/migrate/20150417122318_remove_import_data_from_project.rb @@ -1,5 +1,9 @@ class RemoveImportDataFromProject < ActiveRecord::Migration - def change + def up remove_column :projects, :import_data end + + def down + add_column :projects, :import_data, :text + end end diff --git a/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb new file mode 100644 index 0000000000..3057ea3c68 --- /dev/null +++ b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb @@ -0,0 +1,88 @@ +class RemovePeriodsAtEndsOfUsernames < ActiveRecord::Migration + include Gitlab::ShellAdapter + + class Namespace < ActiveRecord::Base + class << self + def find_by_path_or_name(path) + find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) + end + + def clean_path(path) + path = path.dup + # Get the email username by removing everything after an `@` sign. + path.gsub!(/@.*\z/, "") + # Usernames can't end in .git, so remove it. + path.gsub!(/\.git\z/, "") + # Remove dashes at the start of the username. + path.gsub!(/\A-+/, "") + # Remove periods at the end of the username. + path.gsub!(/\.+\z/, "") + # Remove everything that's not in the list of allowed characters. + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + + # Users with the great usernames of "." or ".." would end up with a blank username. + # Work around that by setting their username to "blank", followed by a counter. + path = "blank" if path.blank? + + counter = 0 + base = path + while Namespace.find_by_path_or_name(path) + counter += 1 + path = "#{base}#{counter}" + end + + path + end + end + end + + def up + changed_paths = {} + + select_all("SELECT id, username FROM users WHERE username LIKE '%.'").each do |user| + username_was = user["username"] + username = Namespace.clean_path(username_was) + changed_paths[username_was] = username + + username = quote_string(username) + execute "UPDATE users SET username = '#{username}' WHERE id = #{user["id"]}" + execute "UPDATE namespaces SET path = '#{username}', name = '#{username}' WHERE type IS NULL AND owner_id = #{user["id"]}" + end + + select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.'").each do |group| + path_was = group["path"] + path = Namespace.clean_path(path_was) + changed_paths[path_was] = path + + path = quote_string(path) + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group["id"]}" + end + + changed_paths.each do |path_was, path| + # Don't attempt to move if original path only contains periods. + next if path_was =~ /\A\.+\z/ + + if gitlab_shell.mv_namespace(path_was, path) + # If repositories moved successfully we need to remove old satellites + # and send update instructions to users. + # However we cannot allow rollback since we moved namespace dir + # So we basically we mute exceptions in next actions + begin + gitlab_shell.rm_satellites(path_was) + # We cannot send update instructions since models and mailers + # can't safely be used from migrations as they may be written for + # later versions of the database. + # send_update_instructions + rescue + # Returning false does not rollback after_* transaction but gives + # us information about failing some of tasks + false + end + else + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') + end + end + end +end diff --git a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb new file mode 100644 index 0000000000..50a9b2439e --- /dev/null +++ b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb @@ -0,0 +1,11 @@ +class AddDefaultProjectVisibililtyToApplicationSettings < ActiveRecord::Migration + def up + add_column :application_settings, :default_project_visibility, :integer + visibility = Settings.gitlab.default_projects_features['visibility_level'] + execute("update application_settings set default_project_visibility = #{visibility}") + end + + def down + remove_column :application_settings, :default_project_visibility + end +end diff --git a/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..281c88d2a7 --- /dev/null +++ b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# This migration is a duplicate of 20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb +# It shold be applied before the index additions to ensure that `name` is case sensitive. + +class GitlabChangeCollationForTagNames < ActiveRecord::Migration + def up + if ActsAsTaggableOn::Utils.using_mysql? + execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;") + end + end +end diff --git a/db/migrate/20150425164647_remove_duplicate_tags.rb b/db/migrate/20150425164647_remove_duplicate_tags.rb new file mode 100644 index 0000000000..13e5038db9 --- /dev/null +++ b/db/migrate/20150425164647_remove_duplicate_tags.rb @@ -0,0 +1,17 @@ +class RemoveDuplicateTags < ActiveRecord::Migration + def up + select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag| + tag_name = quote_string(tag["name"]) + duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]} + origin_tag_id = duplicate_ids.first + duplicate_ids.delete origin_tag_id + + execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})") + execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})") + end + end + + def down + + end +end diff --git a/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..c1b7868151 --- /dev/null +++ b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb @@ -0,0 +1,27 @@ +# This migration comes from acts_as_taggable_on_engine (originally 2) +class AddMissingUniqueIndices < ActiveRecord::Migration + def self.up + add_index :tags, :name, unique: true + + # pre-GitLab v6.7.0 may not have these indices since there were no + # migrations for them + if index_exists?(:taggings, :tag_id) + remove_index :taggings, :tag_id + end + + if index_exists?(:taggings, [:taggable_id, :taggable_type, :context]) + remove_index :taggings, [:taggable_id, :taggable_type, :context] + end + add_index :taggings, + [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type], + unique: true, name: 'taggings_idx' + end + + def self.down + remove_index :tags, :name + + remove_index :taggings, name: 'taggings_idx' + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type, :context] + end +end diff --git a/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..8edb508078 --- /dev/null +++ b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb @@ -0,0 +1,15 @@ +# This migration comes from acts_as_taggable_on_engine (originally 3) +class AddTaggingsCounterCacheToTags < ActiveRecord::Migration + def self.up + add_column :tags, :taggings_count, :integer, default: 0 + + ActsAsTaggableOn::Tag.reset_column_information + ActsAsTaggableOn::Tag.find_each do |tag| + ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings) + end + end + + def self.down + remove_column :tags, :taggings_count + end +end diff --git a/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..71f2d7f433 --- /dev/null +++ b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# This migration comes from acts_as_taggable_on_engine (originally 4) +class AddMissingTaggableIndex < ActiveRecord::Migration + def self.up + add_index :taggings, [:taggable_id, :taggable_type, :context] + end + + def self.down + remove_index :taggings, [:taggable_id, :taggable_type, :context] + end +end diff --git a/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..bfb06bc7cd --- /dev/null +++ b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# This migration comes from acts_as_taggable_on_engine (originally 5) +# This migration is added to circumvent issue #623 and have special characters +# work properly +class ChangeCollationForTagNames < ActiveRecord::Migration + def up + if ActsAsTaggableOn::Utils.using_mysql? + execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;") + end + end +end diff --git a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb new file mode 100644 index 0000000000..8f1b0cc893 --- /dev/null +++ b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb @@ -0,0 +1,11 @@ +class AddDefaultSnippetVisibilityToAppSettings < ActiveRecord::Migration + def up + add_column :application_settings, :default_snippet_visibility, :integer + visibility = Settings.gitlab.default_projects_features['visibility_level'] + execute("update application_settings set default_snippet_visibility = #{visibility}") + end + + def down + remove_column :application_settings, :default_snippet_visibility + end +end diff --git a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb new file mode 100644 index 0000000000..244637e1c4 --- /dev/null +++ b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb @@ -0,0 +1,9 @@ +class RemoveAbandonedGroupMembersRecords < ActiveRecord::Migration + def up + execute("DELETE FROM members WHERE type = 'GroupMember' AND source_id NOT IN(\ + SELECT id FROM namespaces WHERE type='Group')") + end + + def down + end +end diff --git a/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb new file mode 100644 index 0000000000..184e265361 --- /dev/null +++ b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddRestrictedSignupDomainsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :restricted_signup_domains, :text + end +end diff --git a/db/migrate/20150509180749_convert_legacy_reference_notes.rb b/db/migrate/20150509180749_convert_legacy_reference_notes.rb new file mode 100644 index 0000000000..b02605489b --- /dev/null +++ b/db/migrate/20150509180749_convert_legacy_reference_notes.rb @@ -0,0 +1,16 @@ +# Convert legacy Markdown-emphasized notes to the current, non-emphasized format +# +# _mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666_ +# +# becomes +# +# mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666 +class ConvertLegacyReferenceNotes < ActiveRecord::Migration + def up + execute %q{UPDATE notes SET note = trim(both '_' from note) WHERE system = true AND note LIKE '\_%\_'} + end + + def down + # noop + end +end diff --git a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb new file mode 100644 index 0000000000..0097587b4f --- /dev/null +++ b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb @@ -0,0 +1,9 @@ +class AddNoteEventsToWebHooks < ActiveRecord::Migration + def up + add_column :web_hooks, :note_events, :boolean, default: false, null: false + end + + def down + remove_column :web_hooks, :note_events, :boolean + end +end diff --git a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb new file mode 100644 index 0000000000..6a78294f0b --- /dev/null +++ b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddUserOauthApplicationsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :user_oauth_applications, :bool, default: true + end +end diff --git a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb new file mode 100644 index 0000000000..83e0810140 --- /dev/null +++ b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb @@ -0,0 +1,5 @@ +class AddAfterSignOutPathForApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :after_sign_out_path, :string + end +end \ No newline at end of file diff --git a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb new file mode 100644 index 0000000000..61ff0af41f --- /dev/null +++ b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb @@ -0,0 +1,7 @@ +class AddSessionExpireDelayForApplicationSettings < ActiveRecord::Migration + def change + unless column_exists?(:application_settings, :session_expire_delay) + add_column :application_settings, :session_expire_delay, :integer, default: 10080, null: false + end + end +end diff --git a/db/migrate/20150610065936_add_dashboard_to_users.rb b/db/migrate/20150610065936_add_dashboard_to_users.rb new file mode 100644 index 0000000000..2628e45072 --- /dev/null +++ b/db/migrate/20150610065936_add_dashboard_to_users.rb @@ -0,0 +1,9 @@ +class AddDashboardToUsers < ActiveRecord::Migration + def up + add_column :users, :dashboard, :integer, default: 0 + end + + def down + remove_column :users, :dashboard + end +end diff --git a/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb b/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb new file mode 100644 index 0000000000..8eed8678b2 --- /dev/null +++ b/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb @@ -0,0 +1,11 @@ +class AddDefaultOtpRequiredForLoginValue < ActiveRecord::Migration + def up + execute %q{UPDATE users SET otp_required_for_login = FALSE WHERE otp_required_for_login IS NULL} + + change_column :users, :otp_required_for_login, :boolean, default: false, null: false + end + + def down + change_column :users, :otp_required_for_login, :boolean, null: true + end +end diff --git a/db/migrate/20150713160110_add_project_view_to_users.rb b/db/migrate/20150713160110_add_project_view_to_users.rb new file mode 100644 index 0000000000..fe3d206df8 --- /dev/null +++ b/db/migrate/20150713160110_add_project_view_to_users.rb @@ -0,0 +1,5 @@ +class AddProjectViewToUsers < ActiveRecord::Migration + def change + add_column :users, :project_view, :integer, default: 0 + end +end diff --git a/db/migrate/20150717130904_add_commits_count_to_project.rb b/db/migrate/20150717130904_add_commits_count_to_project.rb new file mode 100644 index 0000000000..9b46daa593 --- /dev/null +++ b/db/migrate/20150717130904_add_commits_count_to_project.rb @@ -0,0 +1,5 @@ +class AddCommitsCountToProject < ActiveRecord::Migration + def change + add_column :projects, :commit_count, :integer, default: 0 + end +end diff --git a/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb new file mode 100644 index 0000000000..78d45c7f96 --- /dev/null +++ b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb @@ -0,0 +1,7 @@ +class AddUpdatedByToIssuablesAndNotes < ActiveRecord::Migration + def change + add_column :notes, :updated_by_id, :integer + add_column :issues, :updated_by_id, :integer + add_column :merge_requests, :updated_by_id, :integer + end +end diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb new file mode 100644 index 0000000000..e97dc4cf04 --- /dev/null +++ b/db/migrate/20150806104937_create_abuse_reports.rb @@ -0,0 +1,11 @@ +class CreateAbuseReports < ActiveRecord::Migration + def change + create_table :abuse_reports do |t| + t.integer :reporter_id + t.integer :user_id + t.text :message + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1aee37b2e6..6e919f2883 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,19 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150417122318) do +ActiveRecord::Schema.define(version: 20150806104937) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "abuse_reports", force: true do |t| + t.integer "reporter_id" + t.integer "user_id" + t.text "message" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "application_settings", force: true do |t| t.integer "default_projects_limit" t.boolean "signup_enabled" @@ -28,9 +36,30 @@ ActiveRecord::Schema.define(version: 20150417122318) do t.integer "default_branch_protection", default: 2 t.boolean "twitter_sharing_enabled", default: true t.text "restricted_visibility_levels" - t.integer "max_attachment_size", default: 10, null: false + t.boolean "version_check_enabled", default: true + t.integer "max_attachment_size", default: 10, null: false + t.integer "default_project_visibility" + t.integer "default_snippet_visibility" + t.text "restricted_signup_domains" + t.boolean "user_oauth_applications", default: true + t.string "after_sign_out_path" + t.integer "session_expire_delay", default: 10080, null: false end + create_table "audit_events", force: true do |t| + t.integer "author_id", null: false + t.string "type", null: false + t.integer "entity_id", null: false + t.string "entity_type", null: false + t.text "details" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "audit_events", ["author_id"], name: "index_audit_events_on_author_id", using: :btree + add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree + add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree + create_table "broadcast_messages", force: true do |t| t.text "message", null: false t.datetime "starts_at" @@ -107,12 +136,13 @@ ActiveRecord::Schema.define(version: 20150417122318) do t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "position", default: 0 + t.integer "position", default: 0 t.string "branch_name" t.text "description" t.integer "milestone_id" t.string "state" t.integer "iid" + t.integer "updated_by_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -209,6 +239,7 @@ ActiveRecord::Schema.define(version: 20150417122318) do t.text "description" t.integer "position", default: 0 t.datetime "locked_at" + t.integer "updated_by_id" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -268,6 +299,7 @@ ActiveRecord::Schema.define(version: 20150417122318) do t.integer "noteable_id" t.boolean "system", default: false, null: false t.text "st_diff" + t.integer "updated_by_id" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -353,6 +385,7 @@ ActiveRecord::Schema.define(version: 20150417122318) do t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" + t.integer "commit_count", default: 0 end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree @@ -431,13 +464,16 @@ ActiveRecord::Schema.define(version: 20150417122318) do t.datetime "created_at" end - add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree + add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree create_table "tags", force: true do |t| - t.string "name" + t.string "name" + t.integer "taggings_count", default: 0 end + add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree + create_table "users", force: true do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -486,7 +522,14 @@ ActiveRecord::Schema.define(version: 20150417122318) do t.string "bitbucket_access_token" t.string "bitbucket_access_token_secret" t.string "location" + t.string "encrypted_otp_secret" + t.string "encrypted_otp_secret_iv" + t.string "encrypted_otp_secret_salt" + t.boolean "otp_required_for_login", default: false, null: false + t.text "otp_backup_codes" t.string "public_email", default: "", null: false + t.integer "dashboard", default: 0 + t.integer "project_view", default: 0 end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -521,6 +564,7 @@ ActiveRecord::Schema.define(version: 20150417122318) do t.boolean "issues_events", default: false, null: false t.boolean "merge_requests_events", default: false, null: false t.boolean "tag_push_events", default: false + t.boolean "note_events", default: false, null: false end add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree diff --git a/doc/README.md b/doc/README.md index 4e00dceac2..0524fda3ed 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,29 +3,32 @@ ## User documentation - [API](api/README.md) Automate GitLab via a simple and powerful API. +- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. +- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. +- [Importing to GitLab](workflow/importing/README.md). - [Markdown](markdown/markdown.md) GitLab's advanced formatting system. - [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do. +- [Profile Settings](profile/README.md) - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. -- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. ## Administrator documentation +- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. - [Install](install/README.md) Requirements, directory structures and installation from source. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. -- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. -- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. -- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. -- [Security](security/README.md) Learn what you can do to further secure your GitLab instance. -- [Update](update/README.md) Update guides to upgrade your installation. -- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. +- [Log system](logs/logs.md) Log system. - [Operations](operations/README.md) Keeping GitLab up and running -- [Log system](logs/logs.md) Log system +- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. +- [Security](security/README.md) Learn what you can do to further secure your GitLab instance. +- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. +- [Update](update/README.md) Update guides to upgrade your installation. +- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. ## Contributor documentation diff --git a/doc/api/README.md b/doc/api/README.md index dec530d0b8..f369c3fd97 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -4,8 +4,9 @@ - [Users](users.md) - [Session](session.md) -- [Projects](projects.md) +- [Projects](projects.md) including setting Webhooks - [Project Snippets](project_snippets.md) +- [Services](services.md) - [Repositories](repositories.md) - [Repository Files](repository_files.md) - [Commits](commits.md) @@ -18,6 +19,8 @@ - [Deploy Keys](deploy_keys.md) - [System Hooks](system_hooks.md) - [Groups](groups.md) +- [Namespaces](namespaces.md) +- [Settings](settings.md) ## Clients diff --git a/doc/api/groups.md b/doc/api/groups.md index b5a4b05cca..0b9f6406d8 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1,178 +1,192 @@ -# Groups - -## List project groups - -Get a list of groups. (As user: my groups, as admin: all groups) - -``` -GET /groups -``` - -```json -[ - { - "id": 1, - "name": "Foobar Group", - "path": "foo-bar", - "description": "An interesting group" - } -] -``` - -You can search for groups by name or path, see below. - -## Details of a group - -Get all details of a group. - -``` -GET /groups/:id -``` - -Parameters: - -- `id` (required) - The ID or path of a group - -## New group - -Creates a new project group. Available only for admin. - -``` -POST /groups -``` - -Parameters: - -- `name` (required) - The name of the group -- `path` (required) - The path of the group -- `description` (optional) - The group's description - -## Transfer project to group - -Transfer a project to the Group namespace. Available only for admin - -``` -POST /groups/:id/projects/:project_id -``` - -Parameters: - -- `id` (required) - The ID or path of a group -- `project_id` (required) - The ID of a project - -## Remove group - -Removes group with all projects inside. - -``` -DELETE /groups/:id -``` - -Parameters: - -- `id` (required) - The ID or path of a user group - -## Search for group - -Get all groups that match your string in their name or path. - -``` -GET /groups?search=foobar -``` - -```json -[ - { - "id": 1, - "name": "Foobar Group", - "path": "foo-bar", - "description": "An interesting group" - } -] -``` - -## Group members - -**Group access levels** - -The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: - -``` -GUEST = 10 -REPORTER = 20 -DEVELOPER = 30 -MASTER = 40 -OWNER = 50 -``` - -### List group members - -Get a list of group members viewable by the authenticated user. - -``` -GET /groups/:id/members -``` - -```json -[ - { - "id": 1, - "username": "raymond_smith", - "email": "ray@smith.org", - "name": "Raymond Smith", - "state": "active", - "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 - }, - { - "id": 2, - "username": "john_doe", - "email": "joh@doe.org", - "name": "John Doe", - "state": "active", - "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 - } -] -``` - -### Add group member - -Adds a user to the list of group members. - -``` -POST /groups/:id/members -``` - -Parameters: - -- `id` (required) - The ID or path of a group -- `user_id` (required) - The ID of a user to add -- `access_level` (required) - Project access level - -### Edit group team member - -Updates a group team member to a specified access level. - -``` -PUT /groups/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID of a group -- `user_id` (required) - The ID of a group member -- `access_level` (required) - Project access level - -### Remove user team member - -Removes user from user team. - -``` -DELETE /groups/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID or path of a user group -- `user_id` (required) - The ID of a group member +# Groups + +## List project groups + +Get a list of groups. (As user: my groups, as admin: all groups) + +``` +GET /groups +``` + +```json +[ + { + "id": 1, + "name": "Foobar Group", + "path": "foo-bar", + "description": "An interesting group" + } +] +``` + +You can search for groups by name or path, see below. + +## Details of a group + +Get all details of a group. + +``` +GET /groups/:id +``` + +Parameters: + +- `id` (required) - The ID or path of a group + +## New group + +Creates a new project group. Available only for users who can create groups. + +``` +POST /groups +``` + +Parameters: + +- `name` (required) - The name of the group +- `path` (required) - The path of the group +- `description` (optional) - The group's description + +## Transfer project to group + +Transfer a project to the Group namespace. Available only for admin + +``` +POST /groups/:id/projects/:project_id +``` + +Parameters: + +- `id` (required) - The ID or path of a group +- `project_id` (required) - The ID of a project + +## Remove group + +Removes group with all projects inside. + +``` +DELETE /groups/:id +``` + +Parameters: + +- `id` (required) - The ID or path of a user group + +## Search for group + +Get all groups that match your string in their name or path. + +``` +GET /groups?search=foobar +``` + +```json +[ + { + "id": 1, + "name": "Foobar Group", + "path": "foo-bar", + "description": "An interesting group" + } +] +``` + +## Group members + +**Group access levels** + +The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: + +``` +GUEST = 10 +REPORTER = 20 +DEVELOPER = 30 +MASTER = 40 +OWNER = 50 +``` + +### List group members + +Get a list of group members viewable by the authenticated user. + +``` +GET /groups/:id/members +``` + +```json +[ + { + "id": 1, + "username": "raymond_smith", + "email": "ray@smith.org", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 30 + }, + { + "id": 2, + "username": "john_doe", + "email": "joh@doe.org", + "name": "John Doe", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 30 + } +] +``` + +### Add group member + +Adds a user to the list of group members. + +``` +POST /groups/:id/members +``` + +Parameters: + +- `id` (required) - The ID or path of a group +- `user_id` (required) - The ID of a user to add +- `access_level` (required) - Project access level + +### Edit group team member + +Updates a group team member to a specified access level. + +``` +PUT /groups/:id/members/:user_id +``` + +Parameters: + +- `id` (required) - The ID of a group +- `user_id` (required) - The ID of a group member +- `access_level` (required) - Project access level + +### Remove user team member + +Removes user from user team. + +``` +DELETE /groups/:id/members/:user_id +``` + +Parameters: + +- `id` (required) - The ID or path of a user group +- `user_id` (required) - The ID of a group member + +## Namespaces in groups + +By default, groups only get 20 namespaces at a time because the API results are paginated. + +To get more (up to 100), pass the following as an argument to the API call: +``` +/groups?per_page=100 +``` + +And to switch pages add: +``` +/groups?per_page=100&page=2 +``` \ No newline at end of file diff --git a/doc/api/issues.md b/doc/api/issues.md index a7dd8b74c3..d407bc35d7 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -99,11 +99,13 @@ GET /projects/:id/issues?labels=foo,bar GET /projects/:id/issues?labels=foo,bar&state=opened GET /projects/:id/issues?milestone=1.0.0 GET /projects/:id/issues?milestone=1.0.0&state=opened +GET /projects/:id/issues?iid=42 ``` Parameters: - `id` (required) - The ID of a project +- `iid` (optional) - Return the issue having the given `iid` - `state` (optional) - Return `all` issues or just those that are `opened` or `closed` - `labels` (optional) - Comma-separated list of label names - `milestone` (optional) - Milestone title diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 6a272539e4..bb551fc67f 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -10,11 +10,13 @@ The pagination parameters `page` and `per_page` can be used to restrict the list GET /projects/:id/merge_requests GET /projects/:id/merge_requests?state=opened GET /projects/:id/merge_requests?state=all +GET /projects/:id/merge_requests?iid=42 ``` Parameters: - `id` (required) - The ID of a project +- `iid` (optional) - Return the request having the given `iid` - `state` (optional) - Return `all` requests or just those that are `merged`, `opened` or `closed` - `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` @@ -47,7 +49,8 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, - "description":"fixed login page css paddings" + "description":"fixed login page css paddings", + "work_in_progress": false } ] ``` @@ -92,7 +95,8 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, - "description":"fixed login page css paddings" + "description":"fixed login page css paddings", + "work_in_progress": false } ``` @@ -116,6 +120,7 @@ Parameters: "project_id": 4, "title": "Blanditiis beatae suscipit hic assumenda et molestias nisi asperiores repellat et.", "description": "Qui voluptatibus placeat ipsa alias quasi. Deleniti rem ut sint. Optio velit qui distinctio.", + "work_in_progress": false, "state": "reopened", "created_at": "2015-02-02T19:49:39.159Z", "updated_at": "2015-02-02T20:08:49.959Z", @@ -219,7 +224,7 @@ If an error occurs, an error number and a message explaining the reason is retur ## Update MR -Updates an existing merge request. You can change branches, title, or even close the MR. +Updates an existing merge request. You can change the target branch, title, or even close the MR. ``` PUT /projects/:id/merge_request/:merge_request_id @@ -229,7 +234,6 @@ Parameters: - `id` (required) - The ID of a project - `merge_request_id` (required) - ID of MR -- `source_branch` - The source branch - `target_branch` - The target branch - `assignee_id` - Assignee user ID - `title` - Title of MR @@ -240,7 +244,6 @@ Parameters: { "id": 1, "target_branch": "master", - "source_branch": "test1", "project_id": 3, "title": "test1", "description": "description1", @@ -336,14 +339,6 @@ Parameters: ```json { - "author": { - "id": 1, - "username": "admin", - "email": "admin@example.com", - "name": "Administrator", - "blocked": false, - "created_at": "2012-04-29T08:46:00Z" - }, "note": "text1" } ``` @@ -388,6 +383,6 @@ Parameters: ] ``` -## Comments on issues +## Comments on merge requets Comments are done via the notes resource. diff --git a/doc/api/milestones.md b/doc/api/milestones.md index d48b3bcce8..a682872826 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -6,6 +6,7 @@ Returns a list of project milestones. ``` GET /projects/:id/milestones +GET /projects/:id/milestones?iid=42 ``` ```json @@ -27,6 +28,7 @@ GET /projects/:id/milestones Parameters: - `id` (required) - The ID of a project +- `iid` (optional) - Return the milestone having the given `iid` ## Get single milestone diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md new file mode 100644 index 0000000000..7b3238441f --- /dev/null +++ b/doc/api/namespaces.md @@ -0,0 +1,44 @@ +# Namespaces + +## List namespaces + +Get a list of namespaces. (As user: my namespaces, as admin: all namespaces) + +``` +GET /namespaces +``` + +```json +[ + { + "id": 1, + "path": "user1", + "kind": "user" + }, + { + "id": 2, + "path": "group1", + "kind": "group" + } +] +``` + +You can search for namespaces by name or path, see below. + +## Search for namespace + +Get all namespaces that match your string in their name or path. + +``` +GET /namespaces?search=foobar +``` + +```json +[ + { + "id": 1, + "path": "user1", + "kind": "user" + } +] +``` diff --git a/doc/api/notes.md b/doc/api/notes.md index ee2f9fa0ea..c683cb883d 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -31,7 +31,10 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "created_at": "2013-10-02T09:22:45Z" + "created_at": "2013-10-02T09:22:45Z", + "system": true, + "upvote": false, + "downvote": false }, { "id": 305, @@ -45,7 +48,10 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "created_at": "2013-10-02T09:56:03Z" + "created_at": "2013-10-02T09:56:03Z", + "system": false, + "upvote": false, + "downvote": false } ] ``` diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index 50e134847c..a7acf37b5b 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -1,5 +1,18 @@ # Project snippets +### Snippet visibility level + +Snippets in GitLab can be either private, internal or public. +You can set it with the `visibility_level` field in the snippet. + +Constants for snippet visibility levels are: + +| Visibility | visibility_level | Description | +| ---------- | ---------------- | ----------- | +| Private | `0` | The snippet is visible only the snippet creator | +| Internal | `10` | The snippet is visible for any logged in user | +| Public | `20` | The snippet can be accessed without any authentication | + ## List snippets Get a list of project snippets. @@ -58,6 +71,7 @@ Parameters: - `title` (required) - The title of a snippet - `file_name` (required) - The name of a snippet file - `code` (required) - The content of a snippet +- `visibility_level` (required) - The snippet's visibility ## Update snippet @@ -74,6 +88,7 @@ Parameters: - `title` (optional) - The title of a snippet - `file_name` (optional) - The name of a snippet file - `code` (optional) - The content of a snippet +- `visibility_level` (optional) - The snippet's visibility ## Delete snippet diff --git a/doc/api/projects.md b/doc/api/projects.md index 971fe96fb8..10533c73a3 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -4,16 +4,16 @@ ### Project visibility level Project in GitLab has be either private, internal or public. -You can determine it by `visibility_level` field in project. +You can determine it by `visibility_level` field in project. Constants for project visibility levels are next: -* Private. `visibility_level` is `0`. +* Private. `visibility_level` is `0`. Project access must be granted explicitly for each user. * Internal. `visibility_level` is `10`. The project can be cloned by any logged in user. - + * Public. `visibility_level` is `20`. The project can be cloned without any authentication. @@ -32,6 +32,7 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ```json [ @@ -134,6 +135,7 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ### List ALL projects @@ -149,6 +151,7 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ### Get single project @@ -359,7 +362,7 @@ Parameters: - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) -On success, method returns 200 with the updated project. If parameters are +On success, method returns 200 with the updated project. If parameters are invalid, 400 is returned. ### Fork project @@ -476,6 +479,9 @@ rely on the returned JSON structure. ## Hooks +Also called Project Hooks and Webhooks. +These are different for [System Hooks](system_hooks.md) that are system wide. + ### List project hooks Get a list of project hooks. diff --git a/doc/api/settings.md b/doc/api/settings.md new file mode 100644 index 0000000000..d1b93a09c0 --- /dev/null +++ b/doc/api/settings.md @@ -0,0 +1,88 @@ +# Application settings + +This API allows you to read and modify GitLab instance application settings. + + +## Get current application settings: + +``` +GET /application/settings +``` + +```json +{ + "id": 1, + "default_projects_limit": 10, + "signup_enabled": true, + "signin_enabled": true, + "gravatar_enabled": true, + "sign_in_text": "", + "created_at": "2015-06-12T15:51:55.432Z", + "updated_at": "2015-06-30T13:22:42.210Z", + "home_page_url": "", + "default_branch_protection": 2, + "twitter_sharing_enabled": true, + "restricted_visibility_levels": [], + "max_attachment_size": 10, + "session_expire_delay": 10080, + "default_project_visibility": 0, + "default_snippet_visibility": 0, + "restricted_signup_domains": [], + "user_oauth_applications": true, + "after_sign_out_path": "" +} +``` + +## Change application settings: + + + +``` +PUT /application/settings +``` + +Parameters: + +- `default_projects_limit` - project limit per user +- `signup_enabled` - enable registration +- `signin_enabled` - enable login via GitLab account +- `gravatar_enabled` - enable gravatar +- `sign_in_text` - text on login page +- `home_page_url` - redirect to this URL when not logged in +- `default_branch_protection` - determine if developers can push to master +- `twitter_sharing_enabled` - allow users to share project creation in twitter +- `restricted_visibility_levels` - restrict certain visibility levels +- `max_attachment_size` - limit attachment size +- `session_expire_delay` - session lifetime +- `default_project_visibility` - what visibility level new project receives +- `default_snippet_visibility` - what visibility level new snippet receives +- `restricted_signup_domains` - force people to use only corporate emails for signup +- `user_oauth_applications` - allow users to create oauth applicaitons +- `after_sign_out_path` - where redirect user after logout + +All parameters are optional. You can send only one that you want to change. + + +```json +{ + "id": 1, + "default_projects_limit": 10, + "signup_enabled": true, + "signin_enabled": true, + "gravatar_enabled": true, + "sign_in_text": "", + "created_at": "2015-06-12T15:51:55.432Z", + "updated_at": "2015-06-30T13:22:42.210Z", + "home_page_url": "", + "default_branch_protection": 2, + "twitter_sharing_enabled": true, + "restricted_visibility_levels": [], + "max_attachment_size": 10, + "session_expire_delay": 10080, + "default_project_visibility": 0, + "default_snippet_visibility": 0, + "restricted_signup_domains": [], + "user_oauth_applications": true, + "after_sign_out_path": "" +} +``` diff --git a/doc/api/users.md b/doc/api/users.md index a8b7685b50..7ba2db248f 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -57,7 +57,9 @@ GET /users "color_scheme_id": 2, "is_admin": false, "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", - "can_create_group": true + "can_create_group": true, + "current_sign_in_at": "2014-03-19T13:12:15Z", + "two_factor_enabled": true }, { "id": 2, @@ -79,7 +81,9 @@ GET /users "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "can_create_group": true, "can_create_project": true, - "projects_limit": 100 + "projects_limit": 100, + "current_sign_in_at": "2014-03-19T17:54:13Z", + "two_factor_enabled": false } ] ``` @@ -392,3 +396,163 @@ Parameters: - `id` (required) - SSH key ID Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found. + +## List emails + +Get a list of currently authenticated user's emails. + +``` +GET /user/emails +``` + +```json +[ + { + "id": 1, + "email": "email@example.com" + }, + { + "id": 3, + "email": "email2@example.com" + } +] +``` + +Parameters: + +- **none** + +## List emails for user + +Get a list of a specified user's emails. Available only for admin + +``` +GET /users/:uid/emails +``` + +Parameters: + +- `uid` (required) - id of specified user + +## Single email + +Get a single email. + +``` +GET /user/emails/:id +``` + +Parameters: + +- `id` (required) - email ID + +```json +{ + "id": 1, + "email": "email@example.com" +} +``` + +## Add email + +Creates a new email owned by the currently authenticated user. + +``` +POST /user/emails +``` + +Parameters: + +- `email` (required) - email address + +```json +{ + "id": 4, + "email": "email@example.com" +} +``` + +Will return created email with status `201 Created` on success. If an +error occurs a `400 Bad Request` is returned with a message explaining the error: + +```json +{ + "message": { + "email": [ + "has already been taken" + ] + } +} +``` + +## Add email for user + +Create new email owned by specified user. Available only for admin + +``` +POST /users/:id/emails +``` + +Parameters: + +- `id` (required) - id of specified user +- `email` (required) - email address + +Will return created email with status `201 Created` on success, or `404 Not found` on fail. + +## Delete email for current user + +Deletes email owned by currently authenticated user. +This is an idempotent function and calling it on a email that is already deleted +or not available results in `200 OK`. + +``` +DELETE /user/emails/:id +``` + +Parameters: + +- `id` (required) - email ID + +## Delete email for given user + +Deletes email owned by a specified user. Available only for admin. + +``` +DELETE /users/:uid/emails/:id +``` + +Parameters: + +- `uid` (required) - id of specified user +- `id` (required) - email ID + +Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found. + +## Block user + +Blocks the specified user. Available only for admin. + +``` +PUT /users/:uid/block +``` + +Parameters: + +- `uid` (required) - id of specified user + +Will return `200 OK` on success, or `404 User Not Found` is user cannot be found. + +## Unblock user + +Unblocks the specified user. Available only for admin. + +``` +PUT /users/:uid/unblock +``` + +Parameters: + +- `uid` (required) - id of specified user + +Will return `200 OK` on success, or `404 User Not Found` is user cannot be found. diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index aa65a082a5..64f128f5a6 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,5 +1,7 @@ # Issue closing pattern +Here's how to close multiple issues in one commit message: + If a commit message matches the regular expression below, all issues referenced from the matched text will be closed. This happens when the commit is pushed or merged into the default branch of a project. @@ -33,4 +35,4 @@ issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:is For manual installs you can customize the pattern in [gitlab.yml][0]. [0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/40c3675372320febf5264061c9bcd63db2dfd13c/config/gitlab.yml.example#L65 -[1]: http://rubular.com/r/Xmbexed1OJ +[1]: http://rubular.com/r/Xmbexed1OJ \ No newline at end of file diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md index ee57fdc659..54c1780c3a 100644 --- a/doc/customization/libravatar.md +++ b/doc/customization/libravatar.md @@ -1,6 +1,6 @@ # Use Libravatar service with GitLab -GitLab by default supports [Gravatar](gravatar.com) avatar service. +GitLab by default supports [Gravatar](https://gravatar.com) avatar service. Libravatar is a service which delivers your avatar (profile picture) to other websites and their API is [heavily based on gravatar](http://wiki.libravatar.org/api/). diff --git a/doc/development/README.md b/doc/development/README.md index d5d264be19..6bc8e1888d 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -1,4 +1,4 @@ -# Development +# Development - [Architecture](architecture.md) of GitLab - [Shell commands](shell_commands.md) in the GitLab codebase @@ -6,3 +6,5 @@ - [CI setup](ci_setup.md) for testing GitLab - [Sidekiq debugging](sidekiq_debugging.md) - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements +- [Migration Style Guide](migration_style_guide.md) for creating safe migrations +- [How to dump production data to staging](dump_db.md) diff --git a/doc/development/db_dump.md b/doc/development/db_dump.md new file mode 100644 index 0000000000..21f1b3edec --- /dev/null +++ b/doc/development/db_dump.md @@ -0,0 +1,50 @@ +# Importing a database dump into a staging enviroment + +Sometimes it is useful to import the database from a production environment +into a staging environment for testing. The procedure below assumes you have +SSH+sudo access to both the production environment and the staging VM. + +**Destroy your staging VM** when you are done with it. It is important to avoid +data leaks. + +On the staging VM, add the following line to `/etc/gitlab/gitlab.rb` to speed up +large database imports. + +``` +# On STAGING +echo "postgresql['checkpoint_segments'] = 64" | sudo tee -a /etc/gitlab/gitlab.rb +sudo touch /etc/gitlab/skip-auto-migrations +sudo gitlab-ctl reconfigure +sudo gitlab-ctl stop unicorn +sudo gitlab-ctl stop sidekiq +``` + +Next, we let the production environment stream a compressed SQL dump to our +local machine via SSH, and redirect this stream to a psql client on the staging +VM. + +``` +# On LOCAL MACHINE +ssh -C gitlab.example.com sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -Cc gitlabhq_production |\ + ssh -C staging-vm sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql -d template1 +``` + +## Recreating directory structure + +If you need to re-create some directory structure on the staging server you can +use this procedure. + +First, on the production server, create a list of directories you want to +re-create. + +``` +# On PRODUCTION +(umask 077; sudo find /var/opt/gitlab/git-data/repositories -maxdepth 1 -type d -print0 > directories.txt) +``` + +Copy `directories.txt` to the staging server and create the directories there. + +``` +# On STAGING +sudo -u git xargs -0 mkdir -p < directories.txt +``` diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md new file mode 100644 index 0000000000..4fa1961fde --- /dev/null +++ b/doc/development/migration_style_guide.md @@ -0,0 +1,88 @@ +# Migration Style Guide + +When writing migrations for GitLab, you have to take into account that +these will be ran by hundreds of thousands of organizations of all sizes, some with +many years of data in their database. + +In addition, having to take a server offline for a an upgrade small or big is +a big burden for most organizations. For this reason it is important that your +migrations are written carefully, can be applied online and adhere to the style guide below. + +When writing your migrations, also consider that databases might have stale data +or inconsistencies and guard for that. Try to make as little assumptions as possible +about the state of the database. + +Please don't depend on GitLab specific code since it can change in future versions. +If needed copy-paste GitLab code into the migration to make make it forward compatible. + +## Comments in the migration + +Each migration you write needs to have the two following pieces of information +as comments. + +### Online, Offline, errors? + +First, you need to provide information on whether the migration can be applied: + +1. online without errors (works on previous version and new one) +2. online with errors on old instances after migrating +3. online with errors on new instances while migrating +4. offline (needs to happen without app servers to prevent db corruption) + +It is always preferable to have a migration run online. If you expect the migration +to take particularly long (for instance, if it loops through all notes), +this is valuable information to add. + +### Reversibility + +Your migration should be reversible. This is very important, as it should +be possible to downgrade in case of a vulnerability or bugs. + +In your migration, add a comment describing how the reversibility of the +migration was tested. + + +## Removing indices + +If you need to remove index, please add a condition like in following example: + +``` +remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) +``` + +## Adding indices + +If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation. + +## Testing + +Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct. + +Make sure your migration can be reversed. + +## Data migration + +Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of using plain SQL you need to quote all input manually with `quote_string` helper. + +Example with Arel: + +``` +users = Arel::Table.new(:users) +users.group(users[:user_id]).having(users[:id].count.gt(5)) + +#updtae other tables with this results +``` + +Example with plain SQL and `quote_string` helper: + +``` +select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag| + tag_name = quote_string(tag["name"]) + duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]} + origin_tag_id = duplicate_ids.first + duplicate_ids.delete origin_tag_id + + execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})") + execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})") +end +``` diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 821027f43f..2d1d0fb415 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -177,3 +177,33 @@ File.open(full_path) do # Etc. ``` A check like this could have avoided CVE-2013-4583. + +## Properly anchor regular expressions to the start and end of strings + +When using regular expressions to validate user input that is passed as an argument to a shell command, make sure to use the `\A` and `\z` anchors that designate the start and end of the string, rather than `^` and `$`, or no anchors at all. + +If you don't, an attacker could use this to execute commands with potentially harmful effect. + +For example, when a project's `import_url` is validated like below, the user could trick GitLab into cloning from a Git repository on the local filesystem. + +```ruby +validates :import_url, format: { with: URI.regexp(%w(ssh git http https)) } +# URI.regexp(%w(ssh git http https)) roughly evaluates to /(ssh|git|http|https):(something_that_looks_like_a_url)/ +``` + +Suppose the user submits the following as their import URL: + +``` +file://git:/tmp/lol +``` + +Since there are no anchors in the used regular expression, the `git:/tmp/lol` in the value would match, and the validation would pass. + +When importing, GitLab would execute the following command, passing the `import_url` as an argument: + + +```sh +git clone file://git:/tmp/lol +``` + +Git would simply ignore the `git:` part, interpret the path as `file:///tmp/lol` and import the repository into the new project, in turn potentially giving the attacker access to any repository in the system, whether private or not. diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md new file mode 100644 index 0000000000..b904c70e98 --- /dev/null +++ b/doc/gitlab-basics/README.md @@ -0,0 +1,25 @@ +# GitLab basics + +Step-by-step guides on the basics of working with Git and GitLab. + +* [Start using Git on the command line](start-using-git.md) + +* [Create and add your SSH Keys](create-your-ssh-keys.md) + +* [Command Line basic commands](command-line-commands.md) + +* [Basic Git commands](basic-git-commands.md) + +* [Create a project](create-project.md) + +* [Create a group](create-group.md) + +* [Create a branch](create-branch.md) + +* [Fork a project](fork-project.md) + +* [Add a file](add-file.md) + +* [Add an image](add-image.md) + +* [Create a Merge Request](add-merge-request.md) diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md new file mode 100644 index 0000000000..57136ac5c3 --- /dev/null +++ b/doc/gitlab-basics/add-file.md @@ -0,0 +1,31 @@ +# How to add a file + +You can create a file in your [shell](command-line-commands.md) or in GitLab. + +To create a file in GitLab, sign in to GitLab. + +Select a project on the right side of your screen: + +![Select a project](basicsimages/select_project.png) + +It's a good idea to [create a branch](create-branch.md), but it's not necessary. + +Go to the directory where you'd like to add the file and click on the "+" sign next to the name of the project and directory: + +![Create a file](basicsimages/create_file.png) + +Name your file (you can't add spaces, so you can use hyphens or underscores). Don't forget to include the markup language you'd like to use : + +![File name](basicsimages/file_name.png) + +Add all the information that you'd like to include in your file: + +![Add information](basicsimages/white_space.png) + +Add a commit message based on what you just added and then click on "commit changes": + +![Commit changes](basicsimages/commit_changes.png) + +### Note +Besides its regular files, every directory needs a README.md or README.html file which works like an index, telling +what the directory is about. It's the first document you'll find when you open a directory. diff --git a/doc/gitlab-basics/add-image.md b/doc/gitlab-basics/add-image.md new file mode 100644 index 0000000000..476b48a217 --- /dev/null +++ b/doc/gitlab-basics/add-image.md @@ -0,0 +1,62 @@ +# How to add an image + +The following are the steps to add images to your repository in +GitLab: + +Find the image that you’d like to add. + +In your computer files, find the GitLab project to which you'd like to add the image +(you'll find it as a regular file). Click on every file until you find exactly where you'd +like to add the image. There, paste the image. + +Go to your [shell](command-line-commands.md), and add the following commands: + +Add this command for every directory that you'd like to open: +``` +cd NAME-OF-FILE-YOU'D-LIKE-TO-OPEN +``` + +Create a new branch: +``` +git checkout -b NAME-OF-BRANCH +``` + +Check if your image was correctly added to the directory: +``` +ls +``` + +You should see the name of the image in the list shown. + +Move up the hierarchy through directories: +``` +cd ../ +``` + +Check the status and you should see your image’s name in red: +``` +git status +``` + +Add your changes: +``` +git add NAME-OF-YOUR-IMAGE +``` + +Check the status and you should see your image’s name in green: +``` +git status +``` + +Add the commit: +``` +git commit -m “DESCRIBE COMMIT IN A FEW WORDS” +``` + +Now you can push (send) your changes (in the branch NAME-OF-BRANCH) to GitLab (the git remote named 'origin'): +``` +git push origin NAME-OF-BRANCH +``` + +Your image will be added to your branch in your repository in GitLab. Create a [Merge Request](add-merge-request.md) +to integrate your changes to your project. diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md new file mode 100644 index 0000000000..236b4248ea --- /dev/null +++ b/doc/gitlab-basics/add-merge-request.md @@ -0,0 +1,42 @@ +# How to create a merge request + +Merge Requests are useful to integrate separate changes that you've made to a project, on different branches. + +To create a new Merge Request, sign in to GitLab. + +Go to the project where you'd like to merge your changes: + +![Select a project](basicsimages/select_project.png) + +Click on "Merge Requests" on the left side of your screen: + +![Merge requests](basicsimages/merge_requests.png) + +Click on "+ new Merge Request" on the right side of the screen: + +![New Merge Request](basicsimages/new_merge_request.png) + +Select a source branch or branch: + +![Select a branch](basicsimages/select_branch.png) + +Click on the "compare branches" button: + +![Compare branches](basicsimages/compare_branches.png) + +Add a title and a description to your Merge Request: + +![Add a title and description](basicsimages/title_description_mr.png) + +Select a user to review your Merge Request and to accept or close it. You may also select milestones and labels (they are optional). Then click on the "submit new Merge Request" button: + +![Add a new merge request](basicsimages/add_new_merge_request.png) + +Your Merge Request will be ready to be approved and published. + +### Note + +After you created a new branch, you'll immediately find a "create a Merge Request" button at the top of your screen. +You may automatically create a Merge Request from your recently created branch when clicking on this button: + +![Automatic MR button](basicsimages/button-create-mr.png) diff --git a/doc/gitlab-basics/basic-git-commands.md b/doc/gitlab-basics/basic-git-commands.md new file mode 100644 index 0000000000..2b5767dd2d --- /dev/null +++ b/doc/gitlab-basics/basic-git-commands.md @@ -0,0 +1,59 @@ +# Basic Git commands + +### Go to the master branch to pull the latest changes from there +``` +git checkout master +``` + +### Download the latest changes in the project +This is for you to work on an up-to-date copy (it is important to do every time you work on a project), while you setup tracking branches. +``` +git pull REMOTE NAME-OF-BRANCH -u +``` +(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch) + +### Create a branch +Spaces won't be recognized, so you need to use a hyphen or underscore. +``` +git checkout -b NAME-OF-BRANCH +``` + +### Work on a branch that has already been created +``` +git checkout NAME-OF-BRANCH +``` + +### View the changes you've made +It's important to be aware of what's happening and what's the status of your changes. +``` +git status +``` + +### Add changes to commit +You'll see your changes in red when you type "git status". +``` +git add CHANGES IN RED +git commit -m "DESCRIBE THE INTENTION OF THE COMMIT" +``` + +### Send changes to gitlab.com +``` +git push REMOTE NAME-OF-BRANCH +``` + +### Delete all changes in the Git repository, but leave unstaged things +``` +git checkout . +``` + +### Delete all changes in the Git repository, including untracked files +``` +git clean -f +``` + +### Merge created branch with master branch +You need to be in the created branch. +``` +git checkout NAME-OF-BRANCH +git merge master +``` diff --git a/doc/gitlab-basics/basicsimages/add_new_merge_request.png b/doc/gitlab-basics/basicsimages/add_new_merge_request.png new file mode 100644 index 0000000000..9d93b217a5 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/add_new_merge_request.png differ diff --git a/doc/gitlab-basics/basicsimages/add_sshkey.png b/doc/gitlab-basics/basicsimages/add_sshkey.png new file mode 100644 index 0000000000..2dede97aa4 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/add_sshkey.png differ diff --git a/doc/gitlab-basics/basicsimages/branch_info.png b/doc/gitlab-basics/basicsimages/branch_info.png new file mode 100644 index 0000000000..c5e38b552a Binary files /dev/null and b/doc/gitlab-basics/basicsimages/branch_info.png differ diff --git a/doc/gitlab-basics/basicsimages/branch_name.png b/doc/gitlab-basics/basicsimages/branch_name.png new file mode 100644 index 0000000000..06e77f5eea Binary files /dev/null and b/doc/gitlab-basics/basicsimages/branch_name.png differ diff --git a/doc/gitlab-basics/basicsimages/branches.png b/doc/gitlab-basics/basicsimages/branches.png new file mode 100644 index 0000000000..c18fa83b96 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/branches.png differ diff --git a/doc/gitlab-basics/basicsimages/button-create-mr.png b/doc/gitlab-basics/basicsimages/button-create-mr.png new file mode 100644 index 0000000000..457af459bb Binary files /dev/null and b/doc/gitlab-basics/basicsimages/button-create-mr.png differ diff --git a/doc/gitlab-basics/basicsimages/click-on-new-group.png b/doc/gitlab-basics/basicsimages/click-on-new-group.png new file mode 100644 index 0000000000..94b6d5756d Binary files /dev/null and b/doc/gitlab-basics/basicsimages/click-on-new-group.png differ diff --git a/doc/gitlab-basics/basicsimages/commit_changes.png b/doc/gitlab-basics/basicsimages/commit_changes.png new file mode 100644 index 0000000000..81588336f3 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/commit_changes.png differ diff --git a/doc/gitlab-basics/basicsimages/commit_message.png b/doc/gitlab-basics/basicsimages/commit_message.png new file mode 100644 index 0000000000..0df2c32653 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/commit_message.png differ diff --git a/doc/gitlab-basics/basicsimages/commits.png b/doc/gitlab-basics/basicsimages/commits.png new file mode 100644 index 0000000000..7e60653907 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/commits.png differ diff --git a/doc/gitlab-basics/basicsimages/compare_braches.png b/doc/gitlab-basics/basicsimages/compare_braches.png new file mode 100644 index 0000000000..7eebaed907 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/compare_braches.png differ diff --git a/doc/gitlab-basics/basicsimages/create_file.png b/doc/gitlab-basics/basicsimages/create_file.png new file mode 100644 index 0000000000..688e355cca Binary files /dev/null and b/doc/gitlab-basics/basicsimages/create_file.png differ diff --git a/doc/gitlab-basics/basicsimages/create_group.png b/doc/gitlab-basics/basicsimages/create_group.png new file mode 100644 index 0000000000..57da898abd Binary files /dev/null and b/doc/gitlab-basics/basicsimages/create_group.png differ diff --git a/doc/gitlab-basics/basicsimages/edit_file.png b/doc/gitlab-basics/basicsimages/edit_file.png new file mode 100644 index 0000000000..afa6876010 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/edit_file.png differ diff --git a/doc/gitlab-basics/basicsimages/file_located.png b/doc/gitlab-basics/basicsimages/file_located.png new file mode 100644 index 0000000000..1def489d16 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/file_located.png differ diff --git a/doc/gitlab-basics/basicsimages/file_name.png b/doc/gitlab-basics/basicsimages/file_name.png new file mode 100644 index 0000000000..9ac2f1c355 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/file_name.png differ diff --git a/doc/gitlab-basics/basicsimages/find_file.png b/doc/gitlab-basics/basicsimages/find_file.png new file mode 100644 index 0000000000..98639149a3 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/find_file.png differ diff --git a/doc/gitlab-basics/basicsimages/find_group.png b/doc/gitlab-basics/basicsimages/find_group.png new file mode 100644 index 0000000000..5ac33c7e95 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/find_group.png differ diff --git a/doc/gitlab-basics/basicsimages/fork.png b/doc/gitlab-basics/basicsimages/fork.png new file mode 100644 index 0000000000..b1f9493861 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/fork.png differ diff --git a/doc/gitlab-basics/basicsimages/group_info.png b/doc/gitlab-basics/basicsimages/group_info.png new file mode 100644 index 0000000000..e78d84e4d8 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/group_info.png differ diff --git a/doc/gitlab-basics/basicsimages/groups.png b/doc/gitlab-basics/basicsimages/groups.png new file mode 100644 index 0000000000..b8104343af Binary files /dev/null and b/doc/gitlab-basics/basicsimages/groups.png differ diff --git a/doc/gitlab-basics/basicsimages/https.png b/doc/gitlab-basics/basicsimages/https.png new file mode 100644 index 0000000000..2a31b4cf75 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/https.png differ diff --git a/doc/gitlab-basics/basicsimages/image_file.png b/doc/gitlab-basics/basicsimages/image_file.png new file mode 100644 index 0000000000..1061d9c508 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/image_file.png differ diff --git a/doc/gitlab-basics/basicsimages/issue_title.png b/doc/gitlab-basics/basicsimages/issue_title.png new file mode 100644 index 0000000000..7b69c70539 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/issue_title.png differ diff --git a/doc/gitlab-basics/basicsimages/issues.png b/doc/gitlab-basics/basicsimages/issues.png new file mode 100644 index 0000000000..9354d05319 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/issues.png differ diff --git a/doc/gitlab-basics/basicsimages/key.png b/doc/gitlab-basics/basicsimages/key.png new file mode 100644 index 0000000000..321805cda9 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/key.png differ diff --git a/doc/gitlab-basics/basicsimages/merge_requests.png b/doc/gitlab-basics/basicsimages/merge_requests.png new file mode 100644 index 0000000000..7601d40de4 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/merge_requests.png differ diff --git a/doc/gitlab-basics/basicsimages/new_issue.png b/doc/gitlab-basics/basicsimages/new_issue.png new file mode 100644 index 0000000000..94e7503dd8 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/new_issue.png differ diff --git a/doc/gitlab-basics/basicsimages/new_merge_request.png b/doc/gitlab-basics/basicsimages/new_merge_request.png new file mode 100644 index 0000000000..9120d2b1ab Binary files /dev/null and b/doc/gitlab-basics/basicsimages/new_merge_request.png differ diff --git a/doc/gitlab-basics/basicsimages/new_project.png b/doc/gitlab-basics/basicsimages/new_project.png new file mode 100644 index 0000000000..ac255270a6 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/new_project.png differ diff --git a/doc/gitlab-basics/basicsimages/newbranch.png b/doc/gitlab-basics/basicsimages/newbranch.png new file mode 100644 index 0000000000..da1a6b604e Binary files /dev/null and b/doc/gitlab-basics/basicsimages/newbranch.png differ diff --git a/doc/gitlab-basics/basicsimages/paste_sshkey.png b/doc/gitlab-basics/basicsimages/paste_sshkey.png new file mode 100644 index 0000000000..9880ddfead Binary files /dev/null and b/doc/gitlab-basics/basicsimages/paste_sshkey.png differ diff --git a/doc/gitlab-basics/basicsimages/profile_settings.png b/doc/gitlab-basics/basicsimages/profile_settings.png new file mode 100644 index 0000000000..5f2e7a7e10 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/profile_settings.png differ diff --git a/doc/gitlab-basics/basicsimages/project_info.png b/doc/gitlab-basics/basicsimages/project_info.png new file mode 100644 index 0000000000..6c06ff351f Binary files /dev/null and b/doc/gitlab-basics/basicsimages/project_info.png differ diff --git a/doc/gitlab-basics/basicsimages/public_file_link.png b/doc/gitlab-basics/basicsimages/public_file_link.png new file mode 100644 index 0000000000..1a60a3d880 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/public_file_link.png differ diff --git a/doc/gitlab-basics/basicsimages/select-group.png b/doc/gitlab-basics/basicsimages/select-group.png new file mode 100644 index 0000000000..d02c2255ff Binary files /dev/null and b/doc/gitlab-basics/basicsimages/select-group.png differ diff --git a/doc/gitlab-basics/basicsimages/select-group2.png b/doc/gitlab-basics/basicsimages/select-group2.png new file mode 100644 index 0000000000..fd40bce499 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/select-group2.png differ diff --git a/doc/gitlab-basics/basicsimages/select_branch.png b/doc/gitlab-basics/basicsimages/select_branch.png new file mode 100644 index 0000000000..3475b2df57 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/select_branch.png differ diff --git a/doc/gitlab-basics/basicsimages/select_project.png b/doc/gitlab-basics/basicsimages/select_project.png new file mode 100644 index 0000000000..6d5aa43912 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/select_project.png differ diff --git a/doc/gitlab-basics/basicsimages/settings.png b/doc/gitlab-basics/basicsimages/settings.png new file mode 100644 index 0000000000..9bf9c5a0d3 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/settings.png differ diff --git a/doc/gitlab-basics/basicsimages/shh_keys.png b/doc/gitlab-basics/basicsimages/shh_keys.png new file mode 100644 index 0000000000..d7ef4dafe7 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/shh_keys.png differ diff --git a/doc/gitlab-basics/basicsimages/submit_new_issue.png b/doc/gitlab-basics/basicsimages/submit_new_issue.png new file mode 100644 index 0000000000..1894441708 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/submit_new_issue.png differ diff --git a/doc/gitlab-basics/basicsimages/title_description_mr.png b/doc/gitlab-basics/basicsimages/title_description_mr.png new file mode 100644 index 0000000000..e08eb62841 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/title_description_mr.png differ diff --git a/doc/gitlab-basics/basicsimages/white_space.png b/doc/gitlab-basics/basicsimages/white_space.png new file mode 100644 index 0000000000..6363a09360 Binary files /dev/null and b/doc/gitlab-basics/basicsimages/white_space.png differ diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md new file mode 100644 index 0000000000..b03cca4029 --- /dev/null +++ b/doc/gitlab-basics/command-line-commands.md @@ -0,0 +1,74 @@ +# Command Line basic commands + +## Start working on your project + +In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to GitLab. + +When you are on your Dashboard, click on the project that you'd like to clone, which you'll find at the right side of your screen. + +![Select a project](basicsimages/select_project.png) + +To work in the project, you can copy a link to the Git repository through a SSH or a HTTPS protocol. SSH is easier to use after it's been [setup](create-your-ssh-keys.md). When you're in the project, click on the HTTPS or SSH button at the right side of your screen. Then copy the link (you'll have to paste it on your shell in the next step). + +![Copy the HTTPS or SSH](basicsimages/https.png) + +## On the command line + +### Clone your project +Go to your computer's shell and type the following command: +``` +git clone PASTE HTTPS OR SSH HERE +``` + +A clone of the project will be created in your computer. + +### Go into a project, directory or file to work in it +``` +cd NAME-OF-PROJECT-OR-FILE +``` + +### Go back one directory or file +``` +cd ../ +``` + +### View what’s in the directory that you are in +``` +ls +``` + +### Create a directory +``` +mkdir NAME-OF-YOUR-DIRECTORY +``` + +### Create a README.md or file in directory +``` +touch README.md +nano README.md +#### ADD YOUR INFORMATION +#### Press: control + X +#### Type: Y +#### Press: enter +``` + +### Remove a file +``` +rm NAME-OF-FILE +``` + +### Remove a directory and all of its contents +``` +rm -rf NAME-OF-DIRECTORY +``` + +### View history in the command line +``` +history +``` + +### Carry out commands for which the account you are using lacks authority +You will be asked for an administrator’s password. +``` +sudo +``` diff --git a/doc/gitlab-basics/create-branch.md b/doc/gitlab-basics/create-branch.md new file mode 100644 index 0000000000..7556b0f663 --- /dev/null +++ b/doc/gitlab-basics/create-branch.md @@ -0,0 +1,39 @@ +# How to create a branch + +A branch is an independent line of development. + +New commits are recorded in the history for the current branch, which results in taking the source from someone’s repository (the place where the history of your work is stored) at certain point in time, and apply your own changes to it in the history of the project. + +To add changes to your GitLab project, you should create a branch. You can do it in your [shell](basic-git-commands.md) or in GitLab. + +To create a new branch in GitLab, sign in and then select a project on the right side of your screen: + +![Select a project](basicsimages/select_project.png) + +Click on "commits" on the menu on the left side of your screen: + +![Commits](basicsimages/commits.png) + +Click on the "branches" tab: + +![Branches](basicsimages/branches.png) + +Click on the "new branch" button on the right side of the screen: + +![New branch](basicsimages/newbranch.png) + +Fill out the information required: + +1. Add a name for your new branch (you can't add spaces, so you can use hyphens or underscores) + +1. On the "create from" space, add the the name of the branch you want to branch off from + +1. Click on the button "create branch" + +![Branch info](basicsimages/branch_info.png) + +### Note: + +You will be able to find and select the name of your branch in the white box next to a project's name: + +![Branch name](basicsimages/branch_name.png) diff --git a/doc/gitlab-basics/create-group.md b/doc/gitlab-basics/create-group.md new file mode 100644 index 0000000000..f80ae62e44 --- /dev/null +++ b/doc/gitlab-basics/create-group.md @@ -0,0 +1,43 @@ +# How to create a group in GitLab + +## Create a group + +Your projects in GitLab can be organized in 2 different ways: +under your own namespace for single projects, such as ´your-name/project-1'; or under groups. +If you organize your projects under a group, it works like a folder. You can manage your group members' permissions and access to the projects. + +To create a group, follow the instructions below: + +Sign in to [GitLab.com](https://gitlab.com). + +When you are on your Dashboard, click on "Groups" on the left menu of your screen: + +![Go to groups](basicsimages/select-group2.png) + +Click on "New group" on the top right side of your screen: + +![New group](basicsimages/click-on-new-group.png) + +Fill out the information required: + +1. Add a group path or group name (you can't add spaces, so you can use hyphens or underscores) + +1. Add details or a group description + +1. You can choose a group avatar if you'd like + +1. Click on "create group" + +![Group information](basicsimages/group_info.png) + +## Add a project to a group + +There are 2 different ways to add a new project to a group: + +* Select a group and then click on "New project" on the right side of your screen. Then you can [create a project](create-project.md) + +![New project](basicsimages/new_project.png) + +* When you are [creating a project](create-project.md), click on "create a group" on the bottom right side of your screen + +![Create a group](basicsimages/create_group.png) diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md new file mode 100644 index 0000000000..b545d62549 --- /dev/null +++ b/doc/gitlab-basics/create-project.md @@ -0,0 +1,21 @@ +# How to create a project in GitLab + +To create a new project, sign in to GitLab. + +Go to your Dashboard and click on "new project" on the right side of your screen. + +![Create a project](basicsimages/new_project.png) + +Fill out the required information: + +1. Project path or the name of your project (you can't add spaces, so you can use hyphens or underscores) + +1. Your project's description + +1. Select a [visibility level](https://gitlab.com/help/public_access/public_access) + +1. You can also [import your existing projects](http://doc.gitlab.com/ce/workflow/importing/README.html) + +1. Click on "create project" + +!![Project information](basicsimages/project_info.png) diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md new file mode 100644 index 0000000000..c8a73feb02 --- /dev/null +++ b/doc/gitlab-basics/create-your-ssh-keys.md @@ -0,0 +1,37 @@ +# How to create your SSH Keys + +You need to connect your computer to your GitLab account through SSH Keys. They are unique for every computer that you link your GitLab account with. + +## Generate your SSH Key + +Create an account on GitLab. Sign up and check your email for your confirmation link. + +After you confirm, go to GitLab and sign in to your account. + +## Add your SSH Key + +At the top right corner, click on "profile settings": + +![profile settings](basicsimages/profile_settings.png) + +On the left side menu click on "SSH Keys": + +![SSH Keys](basicsimages/shh_keys.png) + +Then click on the green button "Add SSH Key": + +![Add SSH Key](basicsimages/add_sshkey.png) + +There, you should paste the SSH Key that your command line will generate for you. Below you'll find the steps to generate it: + +![Paste SSH Key](basicsimages/paste_sshkey.png) + +## To generate an SSH Key on your command line + +Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate it. + +Copy the SSH Key that your command line created and paste it on the "Key" box on the GitLab page. The title will be added automatically. + +![Paste SSH Key](basicsimages/key.png) + +Now, you'll be able to use Git over SSH, instead of Git over HTTP. diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md new file mode 100644 index 0000000000..5f8b81ea91 --- /dev/null +++ b/doc/gitlab-basics/fork-project.md @@ -0,0 +1,19 @@ +# How to fork a project + +A fork is a copy of an original repository that you can put somewhere else +or where you can experiment and apply changes that you can later decide if +publishing or not, without affecting your original project. + +It takes just a few steps to fork a project in GitLab. + +Sign in to GitLab. + +Select a project on the right side of your screen: + +![Select a project](basicsimages/select_project.png) + +Click on the "fork" button on the right side of your screen: + +![Fork](basicsimages/fork.png) + +Click on the user or group to where you'd like to add the forked project. diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md new file mode 100644 index 0000000000..b2ceda025c --- /dev/null +++ b/doc/gitlab-basics/start-using-git.md @@ -0,0 +1,61 @@ +# Start using Git on the command line + +If you want to start using a Git and GitLab, make sure that you have created an account on GitLab. + +## Open a shell + +Depending on your operating system, find the shell of your preference. Here are some suggestions. + +- [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on Mac OSX + +- [GitBash](https://msysgit.github.io) on Windows + +- [Linux Terminal](http://www.howtogeek.com/140679/beginner-geek-how-to-start-using-the-linux-terminal/) on Linux + +## Check if Git has already been installed + +Git is usually preinstalled on Mac and Linux. + +Type the following command and then press enter: +``` +git --version +``` + +You should receive a message that will tell you which Git version you have in your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). + +If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window. + +After you finished installing, open a new shell and type "git --version" again to verify that it was correctly installed. + +## Add your Git username and set your email + +It is important because every Git commit that you create will use this information. + +On your shell, type the following command to add your username: +``` +git config --global user.name ADD YOUR USERNAME +``` + +Then verify that you have the correct username: +``` +git config --global user.name +``` + +To set your email address, type the following command: +``` +git config --global user.email ADD YOUR EMAIL +``` + +To verify that you entered your email correctly, type: +``` +git config --global user.email +``` + +You'll need to do this only once because you are using the "--global" option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the "--global" option when you’re in that project. + +## Check your information + +To view the information that you entered, type: +``` +git config --global --list +``` diff --git a/doc/install/installation.md b/doc/install/installation.md index a61a40ebd1..4b01077884 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -4,6 +4,12 @@ Since an installation from source is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). +One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes. +On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time. +Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://doc.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. +After this termination Runit will detect Sidekiq is not running and will start it. +Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time. + ## Select Version to Install Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the tag (version) of GitLab you would like to install. @@ -56,7 +62,13 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake libkrb5-dev nodejs + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs + +If you want to use Kerberos for user authentication, then install libkrb5-dev: + + sudo apt-get install libkrb5-dev + +**Note:** If you don't know what Kerberos is, you can assume you don't need it. Make sure you have the right version of Git installed @@ -76,8 +88,8 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.1.2.tar.gz | tar xz - cd git-2.1.2/ + curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.4.3.tar.gz | tar xz + cd git-2.4.3/ ./configure make prefix=/usr/local all @@ -183,9 +195,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-10-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-14-stable gitlab -**Note:** You can change `7-10-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -229,10 +241,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Copy the example Rack attack config sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user, useful when editing via web - # Edit user.email according to what is set in gitlab.yml - sudo -u git -H git config --global user.name "GitLab" - sudo -u git -H git config --global user.email "example@example.com" + # Configure Git global settings for git user, used when editing via web editor sudo -u git -H git config --global core.autocrlf input # Configure Redis connection settings @@ -270,17 +279,19 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da **Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](http://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. # For PostgreSQL (note, the option says "without ... mysql") - sudo -u git -H bundle install --deployment --without development test mysql aws + sudo -u git -H bundle install --deployment --without development test mysql aws kerberos # Or if you use MySQL (note, the option says "without ... postgres") - sudo -u git -H bundle install --deployment --without development test postgres aws + sudo -u git -H bundle install --deployment --without development test postgres aws kerberos + +**Note:** If you want to use Kerberos for user authentication, then omit `kerberos` in the `--without` option above. ### Install GitLab Shell GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.2] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.5] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: @@ -288,6 +299,8 @@ GitLab Shell is an SSH access and repository management software developed speci **Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. +**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up gitlab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)". + ### Initialize Database and Activate Advanced Features sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production @@ -355,6 +368,9 @@ Make sure to edit the config file to match your setup: # Change YOUR_SERVER_FQDN to the fully-qualified # domain name of your host serving GitLab. + # If using Ubuntu default nginx install: + # either remove the default_server from the listen line + # or else rm -f /etc/sites-enabled/default sudo editor /etc/nginx/sites-available/gitlab **Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details. diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 7a3216dd2d..a78590d512 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -32,7 +32,7 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.0 or 2.1 +GitLab requires Ruby (MRI) 2.1 You will have to use the standard MRI implementation of Ruby. We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab needs several Gems that have native extensions. @@ -57,6 +57,7 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim - 16 cores supports up to 10,000 users - 32 cores supports up to 20,000 users - 64 cores supports up to 40,000 users +- More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) ### Memory @@ -64,15 +65,17 @@ You need at least 2GB of addressable memory (RAM + swap) to install and use GitL With less memory GitLab will give strange errors during the reconfigure run and 500 errors during usage. - 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advise. -- 1GB RAM + 1GB swap supports up to 100 users -- **2GB RAM** is the **recommended** memory size and supports up to 500 users -- 4GB RAM supports up to 2,000 users -- 8GB RAM supports up to 5,000 users -- 16GB RAM supports up to 10,000 users -- 32GB RAM supports up to 20,000 users -- 64GB RAM supports up to 40,000 users +- 1GB RAM + 1GB swap supports up to 100 users but it will be slow +- **2GB RAM** is the **recommended** memory size and supports up to 100 users +- 4GB RAM supports up to 1,000 users +- 8GB RAM supports up to 2,000 users +- 16GB RAM supports up to 4,000 users +- 32GB RAM supports up to 8,000 users +- 64GB RAM supports up to 16,000 users +- 128GB RAM supports up to 32,000 users +- More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) -Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. +Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. ## Unicorn Workers @@ -90,7 +93,7 @@ To change the Unicorn workers when you have the Omnibus package please see [the ## Database -If you want to run the database separately, the **recommended** database size is **1 MB per user**. +If you want to run the database separately expect a size of about 1 MB per user. ## Redis and Sidekiq @@ -106,4 +109,8 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) -- IE 10+ \ No newline at end of file +- IE 10+ + +### Common UI problems with IE + +If you experience UI issues with Internet Explorer, please make sure that you have the `Compatibility View` mode disabled. diff --git a/doc/integration/README.md b/doc/integration/README.md index 286bd34a0b..6d856951d4 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -7,6 +7,7 @@ See the documentation below for details on how to configure these services. - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. +- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [Slack](slack.md) Integrate with the Slack chat service - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail](gitlab_buttons_in_gmail.md) Adds GitLab actions to messages diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index d82e1f8b41..6a0fa4ce01 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -68,6 +68,8 @@ Bitbucket will generate an application ID and secret key for you to use. 1. Save the configuration file. +1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```). + 1. Restart GitLab for the changes to take effect. On the sign in page there should now be a Bitbucket icon below the regular sign in form. @@ -80,29 +82,7 @@ To allow projects to be imported directly into GitLab, Bitbucket requires two ex Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. -### Step 1: Known hosts - -To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: - -1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: - - ```sh - ssh git@bitbucket.org - ``` - -1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): - - ```sh - The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. - RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. - Are you sure you want to continue connecting (yes/no)? - ``` - -1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. - -1. Your GitLab server is now able to connect to Bitbucket over SSH. Continue to step 2: - -### Step 2: Public key +### Step 1: Public key To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations. @@ -117,6 +97,44 @@ If you have that file in place, you're all set and should see the "Import projec When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`. Make sure to use an **empty passphrase**. -2. Restart GitLab to allow it to find the new public key. +1. Configure SSH client to use your new key: + + Open the SSH configuration file of the git user. + + ```sh + sudo editor /home/git/.ssh/config + ``` + + Add a host configuration for `bitbucket.org`. + + ```sh + Host bitbucket.org + IdentityFile ~/.ssh/bitbucket_rsa + User git + ``` + +### Step 2: Known hosts + +To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: + +1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: + + ```sh + sudo -u git -H ssh bitbucket.org + ``` + +1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): + + ```sh + The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. + RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. + Are you sure you want to continue connecting (yes/no)? + ``` + +1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. + +1. Your GitLab server is now able to connect to Bitbucket over SSH. + +1. Restart GitLab to allow it to find the new public key. You should now see the "Import projects from Bitbucket" option on the New Project page enabled. diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 96755707de..3e660cfba1 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -36,4 +36,9 @@ In GitLab Admin section, navigate to `Service Templates` and choose the service After the template is created, the template details will be pre-filled on the project service page. +NOTE: For each project, you will still need to configure the issue tracking URLs by replacing `:issues_tracker_id` in the above screenshot +with the ID used by your external issue tracker. Prior to GitLab v7.8, this ID was configured in the project settings, and GitLab would automatically +update the URL configured in `gitlab.yml`. This behavior is now depecated, and all issue tracker URLs must be configured directly +within the project's Services settings. + Support to add your commits to the Jira ticket automatically is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jira.html). diff --git a/doc/integration/gitlab_actions.png b/doc/integration/gitlab_actions.png deleted file mode 100644 index b08f54d137..0000000000 Binary files a/doc/integration/gitlab_actions.png and /dev/null differ diff --git a/doc/integration/gitlab_buttons_in_gmail.md b/doc/integration/gitlab_buttons_in_gmail.md deleted file mode 100644 index a9885cef10..0000000000 --- a/doc/integration/gitlab_buttons_in_gmail.md +++ /dev/null @@ -1,28 +0,0 @@ -# GitLab buttons in Gmail - -GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview). - -If correctly setup, emails that require an action will be marked in Gmail. - -![gitlab_actions](gitlab_actions.png) - -To get this functioning, you need to be registered with Google. -[See how to register with google in this document.](https://developers.google.com/gmail/markup/registering-with-google) - -To aid the registering with google, GitLab offers a rake task that will send an email to google whitelisting email address from your GitLab server. - -To check what would be sent to the google email address, run the rake task: - -```bash -bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production -``` - -**This will not send the email but give you the output of how the mail will look.** - -Copy the output of the rake task to [google email markup tester](https://www.google.com/webmasters/markup-tester/u/0/) and press "Validate". - -If you receive "No errors detected" message from the tester you can send the email using: - -```bash -bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production SEND=true -``` diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index b67f793c59..904d5d7fee 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -6,6 +6,13 @@ The first time a user signs in with LDAP credentials, GitLab will create a new G GitLab user attributes such as nickname and email will be copied from the LDAP user entry. +## Security + +GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute. +An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server. + +We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server. + ## Configuring GitLab for LDAP integration To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 24f7b4bb4b..2010cb9b8a 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -75,6 +75,7 @@ Now we can choose one or more of the Supported Providers below to continue confi - [Google](google.md) - [Shibboleth](shibboleth.md) - [Twitter](twitter.md) +- [SAML](saml.md) ## Enable OmniAuth for an Existing User @@ -83,7 +84,7 @@ Existing users can enable OmniAuth for specific providers after the account is c 1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider. 1. Go to profile settings (the silhouette icon in the top right corner). 1. Select the "Account" tab. -1. Under "Social Accounts" select the desired OmniAuth provider, such as Twitter. +1. Under "Connected Accounts" select the desired OmniAuth provider, such as Twitter. 1. The user will be redirected to the provider. Once the user authorized GitLab they will be redirected back to GitLab. The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. diff --git a/doc/integration/saml.md b/doc/integration/saml.md new file mode 100644 index 0000000000..4aa6dbe758 --- /dev/null +++ b/doc/integration/saml.md @@ -0,0 +1,82 @@ +# SAML OmniAuth Provider + +GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as Microsoft ADFS to authenticate users. + +First configure SAML 2.0 support in GitLab, then register the GitLab application in your SAML IdP: + +1. Make sure GitLab is configured with HTTPS. See [Using HTTPS](../install/installation.md#using-https) for instructions. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "saml", + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } } + ``` + +1. Change the value for 'assertion_consumer_service_url' to match the HTTPS endpoint of GitLab (append 'users/auth/saml/callback' to the HTTPS URL of your GitLab installation to generate the correct value). + +1. Change the values of 'idp_cert_fingerprint', 'idp_sso_target_url', 'name_identifier_format' to match your IdP. Check [the omniauth-saml documentation](https://github.com/PracticallyGreen/omniauth-saml) for details on these options. + +1. Change the value of 'issuer' to a unique name, which will identify the application to the IdP. + +1. Restart GitLab for the changes to take effect. + +1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified in 'issuer'. + +To ease configuration, most IdP accept a metadata URL for the application to provide configuration information to the IdP. To build the metadata URL for GitLab, append 'users/auth/saml/metadata' to the HTTPS URL of your GitLab installation, for instance: + ``` + https://gitlab.example.com/users/auth/saml/metadata + ``` + +At a minimum the IdP *must* provide a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. GitLab will also use claims with name 'name', 'first_name', 'last_name' (see [the omniauth-saml gem](https://github.com/PracticallyGreen/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb) for supported claims). + +On the sign in page there should now be a SAML button below the regular sign in form. Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. + +## Troubleshooting + +If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, this likely indicates that GitLab could not get the email address for the SAML user. + +Make sure the IdP provides a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. diff --git a/doc/integration/slack.md b/doc/integration/slack.md index 2fd22c513a..84f1d74c05 100644 --- a/doc/integration/slack.md +++ b/doc/integration/slack.md @@ -16,7 +16,7 @@ To enable Slack integration you must create an Incoming WebHooks integration on 1. Choose the channel name you want to send notifications to -1. Click **Add Incoming WebHooks Integration**Add Integrations. +1. Click **Add Incoming WebHooks Integration** - Optional step; You can change bot's name and avatar by clicking modifying the bot name or avatar under **Integration Settings**. 1. Copy the **Webhook URL**, we'll need this later for GitLab. @@ -32,10 +32,15 @@ After Slack is ready we need to setup GitLab. Here are the steps to achieve this 1. Navigate to Settings -> Services -> Slack -1. Fill in your Slack details +1. Pick the triggers you want to activate +1. Fill in your Slack details + - Webhook: Paste the Webhook URL from the step above + - Username: Fill this in if you want to change the username of the bot + - Channel: Fill this in if you want to change the channel where the messages will be posted - Mark it as active - - Paste in the webhook URL you got from Slack + +1. Save your settings Have fun :) diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index fe9091ad9a..1350c8f693 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -2,9 +2,7 @@ To enable the Twitter OmniAuth provider you must register your application with Twitter. Twitter will generate a client ID and secret key for you to use. -1. Sign in to [Twitter Developers](https://dev.twitter.com/) area. - -1. Hover over the avatar in the top right corner and select "My applications." +1. Sign in to [Twitter Application Management](https://apps.twitter.com/). 1. Select "Create new app" @@ -14,18 +12,18 @@ To enable the Twitter OmniAuth provider you must register your application with - Description: Create a description. - Website: The URL to your GitLab installation. 'https://gitlab.example.com' - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback' - - Agree to the "Rules of the Road." + - Agree to the "Developer Agreement". ![Twitter App Details](twitter_app_details.png) 1. Select "Create your Twitter application." 1. Select the "Settings" tab. -1. Underneath the Callback URL check the box next to "Allow this application to be used to Sign in the Twitter." +1. Underneath the Callback URL check the box next to "Allow this application to be used to Sign in with Twitter." 1. Select "Update settings" at the bottom to save changes. -1. Select the "API Keys" tab. +1. Select the "Keys and Access Tokens" tab. 1. You should now see an API key and API secret (see screenshot). Keep this page open as you continue configuration. @@ -78,4 +76,4 @@ To enable the Twitter OmniAuth provider you must register your application with 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. \ No newline at end of file diff --git a/doc/logs/logs.md b/doc/logs/logs.md index ec0109a426..83c32b0925 100644 --- a/doc/logs/logs.md +++ b/doc/logs/logs.md @@ -5,7 +5,7 @@ In addition to system log files, GitLab Enterprise Edition comes with Audit Even System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. #### production.log -This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/logs/production.log` for installations from the source. +This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/log/production.log` for installations from the source. This file contains information about all performed requests. You can see url and type of request, IP address and what exactly parts of code were involved to service this particular request. Also you can see all SQL request that have been performed and how much time it took. This task is more useful for GitLab contributors and developers. Use part of this log file when you are going to report bug. @@ -30,7 +30,7 @@ Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms) In this example we can see that server processed HTTP request with url `/gitlabhq/yaml_db/tree/master` from IP 168.111.56.1 at 2015-02-12 19:34:53 +0200. Also we can see that request was processed by Projects::TreeController. #### application.log -This file lives in `/var/log/gitlab/gitlab-rails/application.log` for omnibus package or in `/home/git/gitlab/logs/application.log` for installations from the source. +This file lives in `/var/log/gitlab/gitlab-rails/application.log` for omnibus package or in `/home/git/gitlab/log/application.log` for installations from the source. This log file helps you discover events happening in your instance such as user creation, project removing and so on. @@ -42,7 +42,7 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was October 07, 2014 11:25: Project "project133" was removed ``` #### githost.log -This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for omnibus package or in `/home/git/gitlab/logs/githost.log` for installations from the source. +This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for omnibus package or in `/home/git/gitlab/log/githost.log` for installations from the source. The GitLab has to interact with git repositories but in some rare cases something can go wrong and in this case you will know what exactly happened. This log file contains all failed requests from GitLab to git repository. In majority of cases this file will be useful for developers only. ``` @@ -52,7 +52,7 @@ error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/reposit ``` #### satellites.log -This file lives in `/var/log/gitlab/gitlab-rails/satellites.log` for omnibus package or in `/home/git/gitlab/logs/satellites.log` for installations from the source. +This file lives in `/var/log/gitlab/gitlab-rails/satellites.log` for omnibus package or in `/home/git/gitlab/log/satellites.log` for installations from the source. In some cases GitLab should perform write actions to git repository, for example when it is needed to merge the merge request or edit a file with online editor. If something went wrong you can look into this file to find out what exactly happened. ``` @@ -62,7 +62,7 @@ October 07, 2014 11:36: PID: 1872: -> fatal: repository '/Users/vsizov/gitlab-de ``` #### sidekiq.log -This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for omnibus package or in `/home/git/gitlab/logs/sidekiq.log` for installations from the source. +This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for omnibus package or in `/home/git/gitlab/log/sidekiq.log` for installations from the source. GitLab uses background jobs for processing tasks which can take a long time. All information about processing these jobs are writing down to this file. ``` @@ -71,7 +71,7 @@ GitLab uses background jobs for processing tasks which can take a long time. All ``` #### gitlab-shell.log -This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for omnibus package or in `/home/git/gitlab-shell/logs/sidekiq.log` for installations from the source. +This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for installations from the source. gitlab-shell is using by Gitlab for executing git commands and provide ssh access to git repositories. @@ -81,7 +81,7 @@ I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory ``` #### unicorn_stderr.log -This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/logs/unicorn_stderr.log` for installations from the source. +This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from the source. Unicorn is a high-performance forking Web server which is used for serving GitLab application. You can look at this log, for example, if your application does not respond. This log cantains all information about state of unicorn processes at any given time. @@ -99,4 +99,4 @@ W, [2015-02-13T07:16:01.313000 #9094] WARN -- : Unicorn::WorkerKiller send SIGQ I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped # worker=1 I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379 I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready -``` +``` \ No newline at end of file diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 1d5fd4c8b0..322111ae9e 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -66,16 +66,26 @@ It is not reasonable to italicize just _part_ of a word, especially when you're perform_complicated_task do_this_and_do_that_and_another_thing -perform_complicated_task +perform_complicated_task do_this_and_do_that_and_another_thing ## URL auto-linking -GFM will autolink standard URLs you copy and paste into your text. So if you want to link to a URL (instead of a textural link), you can simply put the URL in verbatim and it will be turned into a link to that URL. +GFM will autolink almost any URL you copy and paste into your text. - http://www.google.com + * http://www.google.com + * https://google.com/ + * ftp://ftp.us.debian.org/debian/ + * smb://foo/bar/baz + * irc://irc.freenode.net/gitlab + * http://localhost:3000 -http://www.google.com +* http://www.google.com +* https://google.com/ +* ftp://ftp.us.debian.org/debian/ +* smb://foo/bar/baz +* irc://irc.freenode.net/gitlab +* http://localhost:3000 ## Code and Syntax Highlighting @@ -163,7 +173,7 @@ Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported ## Special GitLab References -GFM recognized special references. +GFM recognizes special references. You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project. @@ -171,31 +181,50 @@ GFM will turn that reference into a link so you can navigate between them easily GFM will recognize the following: -- @foo : for specific team members or groups -- @all : for the whole team -- #123 : for issues -- !123 : for merge requests -- $123 : for snippets -- 1234567 : for commits -- \[file\](path/to/file) : for file references +| input | references | +|:-----------------------|:---------------------------| +| `@user_name` | specific user | +| `@group_name` | specific group | +| `@all` | entire team | +| `#123` | issue | +| `!123` | merge request | +| `$123` | snippet | +| `~123` | label by ID | +| `~bug` | one-word label by name | +| `~"feature request"` | multi-word label by name | +| `9ba12248` | specific commit | +| `9ba12248...b19a04f5` | commit range comparison | +| `[README](doc/README)` | repository file references | -GFM also recognizes references to commits, issues, and merge requests in other projects: +GFM also recognizes certain cross-project references: -- namespace/project#123 : for issues -- namespace/project!123 : for merge requests -- namespace/project@1234567 : for commits +| input | references | +|:----------------------------------------|:------------------------| +| `namespace/project#123` | issue | +| `namespace/project!123` | merge request | +| `namespace/project$123` | snippet | +| `namespace/project@9ba12248` | specific commit | +| `namespace/project@9ba12248...b19a04f5` | commit range comparison | ## Task Lists -You can add task lists to merge request and issue descriptions to keep track of to-do items. To create a task, add an unordered list to the description in an issue or merge request, formatted like so: +You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so: ```no-highlight -* [x] Completed task -* [ ] Unfinished task - * [x] Nested task +- [x] Completed task +- [ ] Incomplete task + - [ ] Sub-task 1 + - [x] Sub-task 2 + - [ ] Sub-task 3 ``` -Task lists can only be created in descriptions, not in titles or comments. Task item state can be managed by editing the description's Markdown or by clicking the rendered checkboxes. +- [x] Completed task +- [ ] Incomplete task + - [ ] Sub-task 1 + - [x] Sub-task 2 + - [ ] Sub-task 3 + +Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes. # Standard Markdown @@ -235,51 +264,38 @@ Alt-H2 ### Header IDs and links -All markdown rendered headers automatically get IDs, except for comments. +All Markdown-rendered headers automatically get IDs, except in comments. On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else. The IDs are generated from the content of the header according to the following rules: -1. remove the heading hashes `#` and process the rest of the line as it would be processed if it were not a header -2. from the result, remove all HTML tags, but keep their inner content -3. convert all characters to lowercase -4. convert all characters except `[a-z0-9_-]` into hyphens `-` -5. transform multiple adjacent hyphens into a single hyphen -6. remove trailing and heading hyphens +1. All text is converted to lowercase +1. All non-word text (e.g., punctuation, HTML) is removed +1. All spaces are converted to hyphens +1. Two or more hyphens in a row are converted to one +1. If a header with the same ID has already been generated, a unique + incrementing number is appended. For example: ``` -###### ..Ab_c-d. e [anchor](URL) ![alt text](URL).. +# This header has spaces in it +## This header has a :thumbsup: in it +# This header has Unicode in it: 한글 +## This header has spaces in it +### This header has spaces in it ``` -which renders as: +Would generate the following link IDs: -###### ..Ab_c-d. e [anchor](URL) ![alt text](URL).. +1. `this-header-has-spaces-in-it` +1. `this-header-has-a-in-it` +1. `this-header-has-unicode-in-it-한글` +1. `this-header-has-spaces-in-it-1` +1. `this-header-has-spaces-in-it-2` -will first be converted by step 1) into a string like: - -``` -..Ab_c-d. e <a href="URL">anchor</a> <img src="URL" alt="alt text"/>.. -``` - -After removing the tags in step 2) we get: - -``` -..Ab_c-d. e anchor .. -``` - -And applying all the other steps gives the id: - -``` -ab_c-d-e-anchor -``` - -Note in particular how: - -- for markdown anchors `[text](URL)`, only the `text` is used -- markdown images `![alt](URL)` are completely ignored +Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID. ## Emphasis @@ -311,8 +327,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~ 1. Ordered sub-list 4. And another item. - Some text that should be aligned with the above item. - * Unordered list can use asterisks - Or minuses + Or pluses @@ -325,12 +339,40 @@ Strikethrough uses two tildes. ~~Scratch this.~~ 1. Ordered sub-list 4. And another item. - Some text that should be aligned with the above item. - * Unordered list can use asterisks - Or minuses + Or pluses +If a list item contains multiple paragraphs, +each subsequent paragraph should be indented with four spaces. + +```no-highlight +1. First ordered list item + + Second paragraph of first item. +2. Another item +``` + +1. First ordered list item + + Second paragraph of first item. +2. Another item + +If the second paragraph isn't indented with four spaces, +the second list item will be incorrectly labeled as `1`. + +```no-highlight +1. First ordered list item + + Second paragraph of first item. +2. Another item +``` + +1. First ordered list item + + Second paragraph of first item. +2. Another item + ## Links There are two ways to create links, inline-style and reference-style. @@ -421,7 +463,7 @@ Quote break. You can also use raw HTML in your Markdown, and it'll mostly work pretty well. -See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows the `class`, `id`, and `style` attributes. +See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements. ```no-highlight
    @@ -525,6 +567,20 @@ Code above produces next output: The row of dashes between the table header and body must have at least three dashes in each column. +By including colons in the header row, you can align the text within that column: + +``` +| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned | +| :----------- | :------: | ------------: | :----------- | :------: | ------------: | +| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | +| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | +``` + +| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned | +| :----------- | :------: | ------------: | :----------- | :------: | ------------: | +| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | +| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | + ## References - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). diff --git a/doc/operations/README.md b/doc/operations/README.md index f1456c6c8e..6a35dab7b6 100644 --- a/doc/operations/README.md +++ b/doc/operations/README.md @@ -2,3 +2,4 @@ - [Sidekiq MemoryKiller](sidekiq_memory_killer.md) - [Cleaning up Redis sessions](cleaning_up_redis_sessions.md) +- [Understanding Unicorn and unicorn-worker-killer](unicorn.md) diff --git a/doc/operations/sidekiq_memory_killer.md b/doc/operations/sidekiq_memory_killer.md index 867b01b0d5..811c2192a1 100644 --- a/doc/operations/sidekiq_memory_killer.md +++ b/doc/operations/sidekiq_memory_killer.md @@ -36,3 +36,5 @@ The MemoryKiller is controlled using environment variables. Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must restart Sidekiq. +- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to 'SIGTERM'. The name of + the final signal sent to the Sidekiq process when we want it to shut down. diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md new file mode 100644 index 0000000000..31b432cd41 --- /dev/null +++ b/doc/operations/unicorn.md @@ -0,0 +1,86 @@ +# Understanding Unicorn and unicorn-worker-killer + +## Unicorn + +GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web +server, to handle web requests (web browsers and Git HTTP clients). Unicorn is +a daemon written in Ruby and C that can load and run a Ruby on Rails +application; in our case the Rails application is GitLab Community Edition or +GitLab Enterprise Edition. + +Unicorn has a multi-process architecture to make better use of available CPU +cores (processes can run on different cores) and to have stronger fault +tolerance (most failures stay isolated in only one process and cannot take down +GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby +environment with the GitLab application code, and then spawns 'workers' which +inherit this clean initial environment. The 'master' never handles any +requests, that is left to the workers. The operating system network stack +queues incoming requests and distributes them among the workers. + +In a perfect world, the master would spawn its pool of workers once, and then +the workers handle incoming web requests one after another until the end of +time. In reality, worker processes can crash or time out: if the master notices +that a worker takes too long to handle a request it will terminate the worker +process with SIGKILL ('kill -9'). No matter how the worker process ended, the +master process will replace it with a new 'clean' process again. Unicorn is +designed to be able to replace 'crashed' workers without dropping user +requests. + +This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The +master process has PID 56227 below. + +``` +[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing +[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped # worker=10 +[2015-06-05T10:58:08.708141 #62538] INFO -- : worker=10 spawned pid=62538 +[2015-06-05T10:58:08.708824 #62538] INFO -- : worker=10 ready +``` + +### Tunables + +The main tunables for Unicorn are the number of worker processes and the +request timeout after which the Unicorn master terminates a worker process. +See the [omnibus-gitlab Unicorn settings +documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md) +if you want to adjust these settings. + +## unicorn-worker-killer + +GitLab has memory leaks. These memory leaks manifest themselves in long-running +processes, such as Unicorn workers. (The Unicorn master process is not known to +leak memory, probably because it does not handle user requests.) + +To make these memory leaks manageable, GitLab comes with the +[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This +gem [monkey-patches](http://en.wikipedia.org/wiki/Monkey_patch) the Unicorn +workers to do a memory self-check after every 16 requests. If the memory of the +Unicorn worker exceeds a pre-set limit then the worker process exits. The +Unicorn master then automatically replaces the worker process. + +This is a robust way to handle memory leaks: Unicorn is designed to handle +workers that 'crash' so no user requests will be dropped. The +unicorn-worker-killer gem is designed to only terminate a worker process _in +between requests_, so no user requests are affected. + +This is what a Unicorn worker memory restart looks like in unicorn_stderr.log. +You see that worker 4 (PID 125918) is inspecting itself and decides to exit. +The threshold memory value was 254802235 bytes, about 250MB. With GitLab this +threshold is a random value between 200 and 250 MB. The master process (PID +117565) then reaps the worker process and spawns a new 'worker 4' with PID +127549. + +``` +[2015-06-05T12:07:41.828374 #125918] WARN -- : #: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes) +[2015-06-05T12:07:41.828472 #125918] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1) +[2015-06-05T12:07:42.025916 #117565] INFO -- : reaped # worker=4 +[2015-06-05T12:07:42.034527 #127549] INFO -- : worker=4 spawned pid=127549 +[2015-06-05T12:07:42.035217 #127549] INFO -- : worker=4 ready +``` + +One other thing that stands out in the log snippet above, taken from +Gitlab.com, is that 'worker 4' was serving requests for only 23 seconds. This +is a normal value for our current GitLab.com setup and traffic. + +The high frequency of Unicorn memory restarts on some GitLab sites can be a +source of confusion for administrators. Usually they are a [red +herring](http://en.wikipedia.org/wiki/Red_herring). diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 8cfa7f9c87..7a6a195844 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -6,6 +6,9 @@ If a user is both in a project group and in the project itself, the highest perm If a user is a GitLab administrator they receive all permissions. +To add or import a user, you can follow the [project users and members +documentation](doc/workflow/add-user/add-user.md). + ## Project | Action | Guest | Reporter | Developer | Master | Owner | @@ -15,6 +18,9 @@ If a user is a GitLab administrator they receive all permissions. | Pull project code | | ✓ | ✓ | ✓ | ✓ | | Download project | | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ | +| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | +| Manage labels | | ✓ | ✓ | ✓ | ✓ | +| Manage merge requests | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | | Push to non-protected branches | | | ✓ | ✓ | ✓ | @@ -22,8 +28,6 @@ If a user is a GitLab administrator they receive all permissions. | Remove non-protected branches | | | ✓ | ✓ | ✓ | | Add tags | | | ✓ | ✓ | ✓ | | Write a wiki | | | ✓ | ✓ | ✓ | -| Manage issue tracker | | | ✓ | ✓ | ✓ | -| Manage labels | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | diff --git a/doc/profile/2fa.png b/doc/profile/2fa.png new file mode 100644 index 0000000000..bbf415210d Binary files /dev/null and b/doc/profile/2fa.png differ diff --git a/doc/profile/2fa_auth.png b/doc/profile/2fa_auth.png new file mode 100644 index 0000000000..4a4fbe6898 Binary files /dev/null and b/doc/profile/2fa_auth.png differ diff --git a/doc/profile/README.md b/doc/profile/README.md new file mode 100644 index 0000000000..6f8359d87f --- /dev/null +++ b/doc/profile/README.md @@ -0,0 +1,4 @@ +# Profile Settings + +- [Preferences](preferences.md) +- [Two-factor Authentication (2FA)](two_factor_authentication.md) diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md new file mode 100644 index 0000000000..f17bbe8f2a --- /dev/null +++ b/doc/profile/preferences.md @@ -0,0 +1,38 @@ +# Profile Preferences + +Settings in the **Profile > Preferences** page allow the user to customize +various aspects of the site to their liking. + +## Application theme + +Changing this setting allows the user to customize the color scheme used for the +navigation bar on the left side of the screen. + +The default is **Charcoal**. + +## Syntax highlighting theme + +Changing this setting allows the user to customize the theme used when viewing +syntax highlighted code on the site. + +The default is **White**. + +## Behavior + +### Default Dashboard + +For users who have access to a large number of projects but only keep up with a +select few, the amount of activity on the default Dashboard page can be +overwhelming. + +Changing this setting allows the user to redefine what their default dashboard +will be. Setting it to **Starred Projects** will make that Dashboard view the +default when signing in or clicking the application logo in the upper left. + +The default is **Your Projects**. + +### Default Project view + +It allows user to choose what content he or she want to see on project page. + +The default is **Readme**. diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md new file mode 100644 index 0000000000..f60ce35d3e --- /dev/null +++ b/doc/profile/two_factor_authentication.md @@ -0,0 +1,72 @@ +# Two-factor Authentication (2FA) + +Two-factor Authentication (2FA) provides an additional level of security to your +GitLab account. Once enabled, in addition to supplying your username and +password to login, you'll be prompted for a code generated by an application on +your phone. + +By enabling 2FA, the only way someone other than you can log into your account +is to know your username and password *and* have access to your phone. + +## Enabling 2FA + +**In GitLab:** + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Enable Two-factor Authentication**. + +![Two-factor setup](2fa.png) + +**On your phone:** + +1. Install a compatible application. We recommend [Google Authenticator] +\(proprietary\) or [FreeOTP] \(open source\). +1. In the application, add a new entry in one of two ways: + * Scan the code with your phone's camera to add the entry automatically. + * Enter the details provided to add the entry manually. + +**In GitLab:** + +1. Enter the six-digit pin number from the entry on your phone into the **Pin + code** field. +1. Click **Submit**. + +If the pin you entered was correct, you'll see a message indicating that +Two-factor Authentication has been enabled, and you'll be presented with a list +of recovery codes. + +## Recovery Codes + +Should you ever lose access to your phone, you can use one of the ten provided +backup codes to login to your account. We suggest copying or printing them for +storage in a safe place. **Each code can be used only once** to log in to your +account. + +If you lose the recovery codes or just want to generate new ones, you can do so +from the **Profile Settings** > **Account** page where you first enabled 2FA. + +## Logging in with 2FA Enabled + +Logging in with 2FA enabled is only slightly different than a normal login. +Enter your username and password credentials as you normally would, and you'll +be presented with a second prompt for an authentication code. Enter the pin from +your phone's application or a recovery code to log in. + +![Two-factor authentication on sign in](2fa_auth.png) + +## Disabling 2FA + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Disable Two-factor Authentication**. + +## Note to GitLab administrators + +You need to take special care to that 2FA keeps working after +[restoring a GitLab backup](../raketasks/backup_restore.md). + +[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en +[FreeOTP]: https://fedorahosted.org/freeotp/ diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md index 780a45bca2..25c0c3ad2a 100644 --- a/doc/project_services/irker.md +++ b/doc/project_services/irker.md @@ -4,43 +4,48 @@ GitLab provides a way to push update messages to an Irker server. When configured, pushes to a project will trigger the service to send data directly to the Irker server. -See the project homepage for further info: http://www.catb.org/esr/irker/ +See the project homepage for further info: https://gitlab.com/esr/irker ## Needed setup You will first need an Irker daemon. You can download the Irker code from its -gitorious repository on https://gitorious.org/irker: `git clone -git@gitorious.org:irker/irker.git`. Once you have downloaded the code, you can -run the python script named `irkerd`. This script is the gateway script, it acts -both as an IRC client, for sending messages to an IRC server obviously, and as a -TCP server, for receiving messages from the GitLab service. +repository on https://gitlab.com/esr/irker: + +``` +git clone https://gitlab.com/esr/irker.git +``` + +Once you have downloaded the code, you can run the python script named `irkerd`. +This script is the gateway script, it acts both as an IRC client, for sending +messages to an IRC server obviously, and as a TCP server, for receiving messages +from the GitLab service. If the Irker server runs on the same machine, you are done. If not, you will need to follow the firsts steps of the next section. -## Optional setup +## Complete these steps in GitLab: -In the `app/models/project_services/irker_service.rb` file, you can modify some -options in the `initialize_settings` method: -- **server_ip** (defaults to `localhost`): the server IP address where the -`irkerd` daemon runs; -- **server_port** (defaults to `6659`): the server port of the `irkerd` daemon; -- **max_channels** (defaults to `3`): the maximum number of recipients the -client is authorized to join, per project; -- **default_irc_uri** (no default) : if this option is set, it has to be in the -format `irc[s]://domain.name` and will be prepend to each and every channel -provided by the user which is not a full URI. - -If the Irker server and the GitLab application do not run on the same host, you -will **need** to setup at least the **server_ip** option. +1. Navigate to the project you want to configure for notifications. +1. Select "Settings" in the top navigation. +1. Select "Services" in the left navigation. +1. Click "Irker". +1. Select the "Active" checkbox. +1. Enter the server host address where `irkerd` runs (defaults to `localhost`) +in the `Server host` field on the Web page +1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the +`Server port` field on the Web page. +1. Optional: if `Default IRC URI` is set, it has to be in the format +`irc[s]://domain.name` and will be prepend to each and every channel provided +by the user which is not a full URI. +1. Specify the recipients (e.g. #channel1, user1, etc.) +1. Save or optionally click "Test Settings". ## Note on Irker recipients Irker accepts channel names of the form `chan` and `#chan`, both for the `#chan` channel. If you want to send messages in query, you will need to add -`,isnick` avec the channel name, in this form: `Aorimn,isnick`. In this latter +`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter case, `Aorimn` is treated as a nick and no more as a channel name. Irker can also join password-protected channels. Users need to append `?key=thesecretpassword` to the chan name. - diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 770b7a70fe..a8dc5c24df 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -7,3 +7,4 @@ - [User management](user_management.md) - [Web hooks](web_hooks.md) - [Import](import.md) of git repositories in bulk +- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators \ No newline at end of file diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 2e41fad89e..6a68c8e828 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -7,7 +7,16 @@ A backup creates an archive file that contains the database, all repositories and all attachments. This archive will be saved in backup_path (see `config/gitlab.yml`). The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup. -You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. +You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. The best way to migrate your repositories from one server to another is through backup restore. + +You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json` +(for omnibus packages) or `/home/git/gitlab/.secret` (for installations +from source). This file contains the database encryption key used +for two-factor authentication. If you restore a GitLab backup without +restoring the database encryption key, users who have two-factor +authentication enabled will loose access to your GitLab server. + +If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)* ``` # use this command if you've installed GitLab with the Omnibus package @@ -17,7 +26,7 @@ sudo gitlab-rake gitlab:backup:create sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` -Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, +Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, uploads (attachments), repositories. Use a comma to specify several options at the same time. ``` @@ -139,22 +148,61 @@ with the name of your bucket: } ``` +## Backup archive permissions + +The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default. +This is meant to avoid other system users reading GitLab's data. +If you need the backup archives to have different permissions you can use the 'archive_permissions' setting. + +``` +# In /etc/gitlab/gitlab.rb, for omnibus packages +gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable +``` + +``` +# In gitlab.yml, for installations from source: + backup: + archive_permissions: 0644 # Makes the backup archives world-readable +``` + ## Storing configuration files -Please be informed that a backup does not store your configuration files. +Please be informed that a backup does not store your configuration +files. One reason for this is that your database contains encrypted +information for two-factor authentication. Storing encrypted +information along with its key in the same place defeats the purpose +of using encryption in the first place! + If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). If you have a cookbook installation there should be a copy of your configuration in Chef. -If you have an installation from source, please consider backing up your `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). +If you have an installation from source, please consider backing up your `.secret` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + +At the very **minimum** you should backup `/etc/gitlab/gitlab-secrets.json` +(Omnibus) or `/home/git/gitlab/.secret` (source) to preserve your +database encryption key. ## Restore a previously created backup You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. -``` -# Omnibus package installation -sudo gitlab-rake gitlab:backup:restore +### Prerequisites -# installation from source +You need to have a working GitLab installation before you can perform +a restore. This is mainly because the system user performing the +restore actions ('git') is usually not allowed to create or delete +the SQL database it needs to import data into ('gitlabhq_production'). +All existing data will be either erased (SQL) or moved to a separate +directory (repositories, uploads). + +If some or all of your GitLab users are using two-factor authentication +(2FA) then you must also make sure to restore +`/etc/gitlab/gitlab-secrets.json` (Omnibus) or `/home/git/gitlab/.secret` +(installations from source). Note that you need to run `gitlab-ctl +reconfigure` after changing `gitlab-secrets.json`. + +### Installation from source + +``` bundle exec rake gitlab:backup:restore RAILS_ENV=production ``` @@ -162,6 +210,7 @@ Options: ``` BACKUP=timestamp_of_backup (required if more than one backup exists) +force=yes (do not ask if the authorized_keys file should get regenerated) ``` Example output: @@ -195,11 +244,45 @@ Restoring repositories: Deleting tmp directories...[DONE] ``` +### Omnibus installations + +We will assume that you have installed GitLab from an omnibus package and run +`sudo gitlab-ctl reconfigure` at least once. + +First make sure your backup tar file is in `/var/opt/gitlab/backups` (or wherever `gitlab_rails['backup_path']` points to). + +```shell +sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/ +``` + +Next, restore the backup by running the restore command. You need to specify the +timestamp of the backup you are restoring. + +```shell +# Stop processes that are connected to the database +sudo gitlab-ctl stop unicorn +sudo gitlab-ctl stop sidekiq + +# This command will overwrite the contents of your GitLab database! +sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186 + +# Start GitLab +sudo gitlab-ctl start + +# Create satellites +sudo gitlab-rake gitlab:satellites:create + +# Check GitLab +sudo gitlab-rake gitlab:check SANITIZE=true +``` + +If there is a GitLab version mismatch between your backup tar file and the installed +version of GitLab, the restore command will abort with an error. Install a package for +the [required version](https://www.gitlab.com/downloads/archives/) and try again. + ## Configure cron to make daily backups -For Omnibus package installations, see https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#scheduling-a-backup . - -For installation from source: +### For installation from source: ``` cd /home/git/gitlab sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups @@ -216,6 +299,32 @@ Add the following lines at the bottom: The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors. This is recommended to reduce cron spam. +### For omnibus installations + +To schedule a cron job that backs up your repositories and GitLab metadata, use the root user: + +``` +sudo su - +crontab -e +``` + +There, add the following line to schedule the backup for everyday at 2 AM: + +``` +0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1 +``` + +You may also want to set a limited lifetime for backups to prevent regular +backups using all your disk space. To do this add the following lines to +`/etc/gitlab/gitlab.rb` and reconfigure: + +``` +# limit backup lifetime to 7 days - 604800 seconds +gitlab_rails['backup_keep_time'] = 604800 +``` + +NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + ## Alternative backup strategies If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow. @@ -238,3 +347,26 @@ Example: LVM snapshots + rsync If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server. It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use. + +## Troubleshooting + +### Restoring database backup using omnibus packages outputs warnings +If you are using backup restore procedures you might encounter the following warnings: + +``` +psql:/var/opt/gitlab/backups/db/database.sql:22: ERROR: must be owner of extension plpgsql +psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurences) +psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurences) + +``` + +Be advised that, backup is successfully restored in spite of these warnings. + +The rake task runs this as the `gitlab` user which does not have the superuser access to the database. When restore is initiated it will also run as `gitlab` user but it will also try to alter the objects it does not have access to. +Those objects have no influence on the database backup/restore but they give this annoying warning. + +For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql). + +## Note +This documentation is for GitLab CE. +We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com. diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 41a994f3f6..69171cd176 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -47,7 +47,6 @@ Git: /usr/bin/git Runs the following rake tasks: -- `gitlab:env:check` - `gitlab:gitlab_shell:check` - `gitlab:sidekiq:check` - `gitlab:app:check` @@ -147,7 +146,7 @@ Do you want to continue (yes/no)? yes ## Clear redis cache -If for some reason the dashboard shows wrong information you might want to +If for some reason the dashboard shows wrong information you might want to clear Redis' cache. For Omnibus-packages: @@ -166,13 +165,18 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production Sometimes during version upgrades you might end up with some wrong CSS or missing some icons. In that case, try to precompile the assets again. -For Omnibus-packages: -``` -sudo gitlab-rake assets:precompile -``` +Note that this only applies to source installations and does NOT apply to +omnibus packages. For installations from source: ``` cd /home/git/gitlab sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ``` + +For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at +the release of upstream GitLab. The omnibus version includes optimized versions +of those assets. Unless you are modifying the JavaScript / CSS code on your +production machine after installing the package, there should be no reason to redo +rake assets:precompile on the production machine. If you suspect that assets +have been corrupted, you should reinstall the omnibus package. diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 80b01ca404..4fbd20762d 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -47,3 +47,12 @@ sudo gitlab-rake gitlab:import:all_users_to_all_groups # installation from source bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production ``` + +## Maintain tight control over the number of active users on your GitLab installation + +- Enable this setting to keep new users blocked until they have been cleared by the admin (default: false). + + +``` +block_auto_created_users: false +``` diff --git a/doc/release/master.md b/doc/release/master.md index 19070b46a0..9163e65200 100644 --- a/doc/release/master.md +++ b/doc/release/master.md @@ -31,3 +31,32 @@ git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git gpa ``` +# Yanking packages from packages.gitlab.com + +In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the +procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg). + +You need to have: + +1. `package_cloud` gem installed (sudo gem install package_cloud) +1. Email and password for packages.gitlab.com +1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io) + +Example of yanking a package: + +```bash +package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm +``` + +If you are attempting this for the first time the output will look something like: + +```bash +Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one. +Email: +marin@gitlab.com +Password: + +Got your token. Writing a config file to /Users/marin/.packagecloud... success! +success! +Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done! +``` diff --git a/doc/release/monthly.md b/doc/release/monthly.md index cfe01896d8..b10e742067 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -6,10 +6,12 @@ It starts 7 working days before the release. The release manager doesn't have to perform all the work but must ensure someone is assigned. The current release manager must schedule the appointment of the next release manager. The new release manager should create overall issue to track the progress. +The release manager should be the only person pushing/merging commits to the x-y-stable branches. ## Release Manager -A release manager is selected that coordinates all releases the coming month, including the patch releases for previous releases. +A release manager is selected that coordinates all releases the coming month, +including the patch releases for previous releases. The release manager has to make sure all the steps below are done and delegated where necessary. This person should also make sure this document is kept up to date and issues are created and updated. @@ -29,18 +31,15 @@ All steps from issue template are explained below ``` Xth: (7 working days before the 22nd) -- [ ] Code freeze -- [ ] Update the CE changelog (#LINK) -- [ ] Update the EE changelog (#LINK) -- [ ] Update the CI changelog (#LINK) - [ ] Triage the omnibus-gitlab milestone Xth: (6 working days before the 22nd) - [ ] Merge CE master in to EE master via merge request (#LINK) - [ ] Determine QA person and notify this person -- [ ] Check the tasks in [how to rc1 guide](howto_rc1.md) and delegate tasks if necessary +- [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary - [ ] Create CE, EE, CI RC1 versions (#LINK) +- [ ] Build RC1 packages (EE first) (#LINK) Xth: (5 working days before the 22nd) @@ -53,7 +52,9 @@ Xth: (4 working days before the 22nd) - [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) - [ ] Update ci.gitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) - [ ] Create regression issues (CE, CI) (#LINK) -- [ ] Tweet about rc1 (#LINK) +- [ ] Tweet about rc1 (#LINK), proposed text: + +> GitLab x.x.0.rc1 is available https://packages.gitlab.com/gitlab/unstable Use at your own risk. Please link regressions issues from LINK_TO_REGRESSION_ISSUE Xth: (3 working days before the 22nd) @@ -61,28 +62,35 @@ Xth: (3 working days before the 22nd) Xth: (2 working days before the 22nd) -- [ ] Check that everyone is mentioned on the blog post (the reviewer should have done this one working day ago) +- [ ] Check that everyone is mentioned on the blog post using `@all` (the reviewer should have done this one working day ago) - [ ] Check that MVP is added to the mvp page (source/mvp/index.html in www-gitlab-com) Xth: (1 working day before the 22nd) -- [ ] Create CE, EE, CI stable versions (#LINK) -- [ ] Create Omnibus tags and build packages -- [ ] Update GitLab.com with the stable version (#LINK) -- [ ] Update ci.gitLab.com with the stable version (#LINK) +- [ ] Merge CE stable into EE stable +- [ ] Create CE, EE, CI release candidates (#LINK) (hopefully final ones with the same commit as the release tomorrow) +- [ ] Create Omnibus tags and build packages for the latest release candidates +- [ ] Update GitLab.com with the latest RC (#LINK) +- [ ] Update ci.gitLab.com with the latest RC (#LINK) -22nd: +22nd before 12AM CET: -- [ ] Release CE, EE and CI (#LINK) +Release before 12AM CET / 3AM PST, to make sure the majority of our users +get the new version on the 22nd and there is sufficient time in the European +workday to quickly fix any issues. + +- [ ] Merge CE stable into EE stable (#LINK) +- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) (#LINK) +- [ ] Create the 'x.y.0' version on version.gitlab.com +- [ ] Try to do before 11AM CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) +- [ ] Try to do before 12AM CET: Publish the release blog post (#LINK) +- [ ] Tweet about the release (blog post) (#LINK) +- [ ] Schedule a second tweet of the release announcement with the same text at 6PM CET / 9AM PST ``` - - - -## Code Freeze - -Stop merging code in master, except for important bug fixes - ## Update changelog Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is @@ -98,6 +106,8 @@ There are three changelogs that need to be updated: CE, EE and CI. Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version, usually X.X.X.pre. +On creating the stable branches, notify the core team and developers. + ## QA Create issue on dev.gitlab.org `gitlab` repository, named "GitLab X.X QA" in order to keep track of the progress. @@ -139,7 +149,8 @@ Tweet about the RC release: ## Prepare the blog post -1. Start with a complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) and fill it out. +1. The blog post template for this release should already exist and might have comments that were added during the month. +1. Fill out as much of the blog post template as you can. 1. Make sure the blog post contains information about the GitLab CI release. 1. Check the changelog of CE and EE for important changes. 1. Also check the CI changelog @@ -152,6 +163,7 @@ Tweet about the RC release: 1. Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master) 1. Assign to one reviewer who will fix spelling issues by editing the branch (either with a git client or by using the online editor) 1. Comment to the reviewer: '@person Please mention the whole team as soon as you are done (3 workdays before release at the latest)' +1. Create a complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) for the release after this. ## Create CE, EE, CI stable versions @@ -209,4 +221,4 @@ Consider creating a post on Hacker News. ## Create a WIP blogpost for the next release -Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). +Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). \ No newline at end of file diff --git a/doc/release/patch.md b/doc/release/patch.md index 4c7b471785..6aa11b283d 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -44,7 +44,7 @@ Create release tag and push to remotes: bundle exec rake release["x.x.x"] ``` -### Release +## Release 1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md) 1. Apply the patch to GitLab.com and the private GitLab development server @@ -52,4 +52,6 @@ bundle exec rake release["x.x.x"] 1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md) 1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post 1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) +1. Create the 'x.y.0' version on version.gitlab.com 1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) +1. Create a new patch release issue for the next potential release \ No newline at end of file diff --git a/doc/security/README.md b/doc/security/README.md index 49dfa6eec7..473f3632dc 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -4,3 +4,4 @@ - [Rack attack](rack_attack.md) - [Web Hooks and insecure internal web services](webhooks.md) - [Information exclusivity](information_exclusivity.md) +- [Reset your root password](reset_root_password.md) \ No newline at end of file diff --git a/doc/security/reset_root_password.md b/doc/security/reset_root_password.md new file mode 100644 index 0000000000..3c13f26267 --- /dev/null +++ b/doc/security/reset_root_password.md @@ -0,0 +1,40 @@ +# How to reset your root password + +Log into your server with root privileges. Then start a Ruby on Rails console. + +Start the console with this command: + +```bash +gitlab-rails console production +``` + +Wait until the console has loaded. + +There are multiple ways to find your user. You can search for email or username. + +```bash +user = User.where(id: 1).first +``` + +or + +```bash +user = User.find_by(email: 'admin@local.host') +``` + +Now you can change your password: + +```bash +user.password = 'secret_pass' +user.password_confirmation = 'secret_pass' +``` + +It's important that you change both password and password_confirmation to make it work. + +Don't forget to save the changes. + +```bash +user.save! +``` + +Exit the console and try to login with your new password. \ No newline at end of file diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 0acb15896d..7cdcd11c04 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -30,7 +30,7 @@ cat ~/.ssh/id_rsa.pub Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your user profile. Please copy the complete key starting with `ssh-` and ending -with your username and host. +with your username and host. Use code below to copy your public key to the clipboard. Depending on your OS you'll need to use a different command: @@ -71,3 +71,40 @@ keys of all the projects you have access to are available. This project access can happen through being a direct member of the project, or through a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more information. + +## Applications + +### Eclipse + +How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration + +## Tip: Non-default OpenSSH key file names or locations + +If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: + +``` +# +# Main gitlab.com server +# +Host gitlab.com +RSAAuthentication yes +IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename +User mygitlabusername +``` + +Another example +``` +# +# Our company's internal Gitlab server +# +Host my-gitlab.company.com +RSAAuthentication yes +IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename +``` + +Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ. + +Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. + +Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll +have when pushing code via SSH. That's why it needs to uniquely map to a single user. diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index f9b6d37d84..b0e4613cde 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -6,6 +6,12 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser ## Hooks request example +**Request header**: + +``` +X-Gitlab-Event: System Hook +``` + **Project created:** ```json diff --git a/doc/update/6.x-or-7.x-to-7.10.md b/doc/update/6.x-or-7.x-to-7.14.md similarity index 91% rename from doc/update/6.x-or-7.x-to-7.10.md rename to doc/update/6.x-or-7.x-to-7.14.md index 39e12f32d0..5bc1f84270 100644 --- a/doc/update/6.x-or-7.x-to-7.10.md +++ b/doc/update/6.x-or-7.x-to-7.14.md @@ -1,7 +1,7 @@ -# From 6.x or 7.x to 7.10 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.10.md) for the most up to date instructions.* +# From 6.x or 7.x to 7.14 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.* -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.10. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.14. ## Global issue numbers @@ -71,7 +71,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash -sudo -u git -H git checkout 7-10-stable +sudo -u git -H git checkout 7-14-stable ``` OR @@ -79,7 +79,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-10-stable-ee +sudo -u git -H git checkout 7-14-stable-ee ``` ## 4. Install additional packages @@ -91,7 +91,8 @@ sudo apt-get install logrotate # Install pkg-config and cmake, which is needed for the latest versions of rugged sudo apt-get install pkg-config cmake -# Install Kerberos header files, which are needed for GitLab EE Kerberos support +# If you want to use Kerberos with GitLab EE for user authentication, install Kerberos header files +# If you don't know what Kerberos is, you can assume you don't need it. sudo apt-get install libkrb5-dev # Install nodejs, javascript runtime required for assets @@ -126,7 +127,7 @@ sudo apt-get install nodejs ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.2 +sudo -u git -H git checkout v2.6.5 ``` ## 7. Install libs, migrations, etc. @@ -161,12 +162,12 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-10-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7.14-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/unicorn.rb.example but with your settings. -* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.5/config.yml.example but with your settings. * Copy rack attack middleware config ```bash @@ -181,10 +182,15 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ### Change Nginx settings -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab-ssl but with your settings. +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/lib/support/nginx/gitlab-ssl but with your settings. * A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. +### Check the version of /usr/local/bin/git + +If you installed Git from source into /usr/local/bin/git then please [check +your version](7.13-to-7.14.md). + ## 9. Start application sudo service gitlab start diff --git a/doc/update/7.10-to-7.11.md b/doc/update/7.10-to-7.11.md new file mode 100644 index 0000000000..79bc6de1e4 --- /dev/null +++ b/doc/update/7.10-to-7.11.md @@ -0,0 +1,103 @@ +# From 7.10 to 7.11 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-11-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-11-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.3 +``` + +### 4. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-10-stable:config/gitlab.yml.example origin/7-11-stable:config/gitlab.yml.example +`````` + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (7.10) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.9 to 7.10](7.9-to-7.10.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/7.11-to-7.12.md b/doc/update/7.11-to-7.12.md new file mode 100644 index 0000000000..cc14a13592 --- /dev/null +++ b/doc/update/7.11-to-7.12.md @@ -0,0 +1,129 @@ +# From 7.11 to 7.12 + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version. + +``` +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-12-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-12-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.3 +``` + +### 5. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 6. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-11-stable:config/gitlab.yml.example origin/7-12-stable:config/gitlab.yml.example +`````` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (7.11) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.10 to 7.11](7.10-to-7.11.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/7.12-to-7.13.md b/doc/update/7.12-to-7.13.md new file mode 100644 index 0000000000..57ebe3261b --- /dev/null +++ b/doc/update/7.12-to-7.13.md @@ -0,0 +1,129 @@ +# From 7.12 to 7.13 + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version. + +``` +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-13-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-13-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.3 +``` + +### 5. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 6. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-12-stable:config/gitlab.yml.example origin/7-13-stable:config/gitlab.yml.example +`````` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (7.12) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.11 to 7.12](7.11-to-7.12.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md new file mode 100644 index 0000000000..6dd9727fb4 --- /dev/null +++ b/doc/update/7.13-to-7.14.md @@ -0,0 +1,129 @@ +# From 7.13 to 7.14 + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version. + +``` +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-14-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-14-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.5 +``` + +### 5. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 6. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-13-stable:config/gitlab.yml.example origin/7-14-stable:config/gitlab.yml.example +`````` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (7.13) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.12 to 7.13](7.12-to-7.13.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/7.6-to-7.7.md b/doc/update/7.6-to-7.7.md index 5924371315..910c7dcdd3 100644 --- a/doc/update/7.6-to-7.7.md +++ b/doc/update/7.6-to-7.7.md @@ -67,7 +67,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab #### New configuration options for `gitlab.yml` -There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. +There are new configuration options available for [`gitlab.yml`](/config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. ``` git diff origin/7-6-stable:config/gitlab.yml.example origin/7-7-stable:config/gitlab.yml.example diff --git a/doc/update/7.8-to-7.9.md b/doc/update/7.8-to-7.9.md index 28fd433e1c..6ffa21c614 100644 --- a/doc/update/7.8-to-7.9.md +++ b/doc/update/7.8-to-7.9.md @@ -42,6 +42,8 @@ sudo -u git -H git checkout v2.6.0 ### 4. Install libs, migrations, etc. +Please refer to the [Node.js setup documentation](https://github.com/joyent/node/wiki/installing-node.js-via-package-manager#debian-and-ubuntu-based-linux-distributions) if you aren't running default GitLab server setup. + ```bash sudo apt-get install nodejs diff --git a/doc/update/7.9-to-7.10.md b/doc/update/7.9-to-7.10.md new file mode 100644 index 0000000000..d1179dc2ec --- /dev/null +++ b/doc/update/7.9-to-7.10.md @@ -0,0 +1,118 @@ +# From 7.9 to 7.10 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-10-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-10-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.2 +``` + +### 4. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-9-stable:config/gitlab.yml.example origin/7-10-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings. +* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. + +#### Setup time zone (optional) + +Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it. + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +### 8. GitHub settings (if applicable) + +If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it +only contains a root URL (ex. `https://gitlab.example.com/`) + +## Things went south? Revert to previous version (7.9) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.8 to 7.9](7.8-to-7.9.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index 50941db25f..a596ea3845 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -1,7 +1,7 @@ # Migrating GitLab from MySQL to Postgres -*Make sure you view this [guide from the `master` branch](../../../master/doc/update/mysql_to_postgresql.md) for the most up to date instructions.* +*Make sure you view this [guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/mysql_to_postgresql.md#migrating-gitlab-from-mysql-to-postgres) for the most up to date instructions.* -If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, import into Postgres and rebuild the indexes as described below. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this. +If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, convert the resulting SQL file, and import it into Postgres. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this. ## Export from MySQL and import into Postgres @@ -14,13 +14,12 @@ sudo service gitlab stop git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab cd mysql-postgresql-converter -mysqldump --compatible=postgresql --default-character-set=utf8 -r databasename.mysql -u root gitlabhq_production -p -python db_converter.py databasename.mysql databasename.psql +mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p +python db_converter.py gitlabhq_production.mysql gitlabhq_production.psql +ed -s gitlabhq_production.psql < move_drop_indexes.ed # Import the database dump as the application database user -sudo -u git psql -f databasename.psql -d gitlabhq_production - -# Rebuild indexes (see below) +sudo -u git psql -f gitlabhq_production.psql -d gitlabhq_production # Install gems for PostgreSQL (note: the line below states '--without ... mysql') sudo -u git -H bundle install --without development test mysql --deployment @@ -28,51 +27,6 @@ sudo -u git -H bundle install --without development test mysql --deployment sudo service gitlab start ``` -## Rebuild indexes - -The lanyrd database converter script does not preserve all indexes, so we have to recreate them ourselves after migrating from MySQL. It is not necessary to shut down GitLab for this process. - -### For non-omnibus installations - -On non-omnibus installations (distributed using Git) we retrieve the index declarations from version control using `git stash`. - -``` -# Clone the database converter on your Postgres-backed GitLab server -cd /tmp -git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab - -cd /home/git/gitlab - -# Stash changes to db/schema.rb to make sure we can find the right index statements -sudo -u git -H git stash - -# Generate add_index.rb -ruby /tmp/mysql-postgresql-converter/add_index_statements.rb db/schema.rb > /tmp/mysql-postgresql-converter/add_index.rb - -# Create the indexes -sudo -u git -H bundle exec rails runner -e production 'eval $stdin.read' < /tmp/mysql-postgresql-converter/add_index.rb -``` - -### For omnibus-gitlab installations - -On omnibus-gitlab we need to get the index declarations from a file called `schema.rb.bundled`. For versions older than 6.9, we need to download the file. - -``` -# Clone the database converter on your Postgres-backed GitLab server -cd /tmp -/opt/gitlab/embedded/bin/git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab -cd /tmp/mysql-postgresql-converter - -# Download schema.rb.bundled if necessary -test -e /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled || sudo /opt/gitlab/embedded/bin/curl -o /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled https://gitlab.com/gitlab-org/gitlab-ce/raw/v6.9.1/db/schema.rb - -# Generate add_index.rb -/opt/gitlab/embedded/bin/ruby add_index_statements.rb /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled > add_index.rb - -# Create the indexes -/opt/gitlab/bin/gitlab-rails runner 'eval $stdin.read' < add_index.rb -``` - ## Converting a GitLab backup file from MySQL to Postgres **Note:** Please make sure to have Python 2.7.x (or higher) installed. @@ -103,14 +57,18 @@ sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter. # Convert gitlabhq_production.mysql sudo -u git -H mkdir db sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_production.mysql db/database.sql +sudo -u git -H ed -s db/database.sql < mysql-postgresql-converter/move_drop_indexes.ed + +# Compress database backup +sudo -u git -H gzip db/database.sql # Replace the MySQL dump in TIMESTAMP_gitlab_backup.tar. # Warning: if you forget to replace TIMESTAMP below, tar will create a new file # 'TIMESTAMP_gitlab_backup.tar' without giving an error. -sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql +sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql.gz # Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab -# installation. Remember to recreate the indexes after the import. +# installation. ``` diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index e29ee2a7b3..22b9be059d 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -22,6 +22,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ```bash cd /home/git/gitlab sudo -u git -H git fetch --all +sudo -u git -H git checkout -- Gemfile.lock db/schema.rb sudo -u git -H git checkout LATEST_TAG ``` diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index f62a53d334..6854250dab 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -1,5 +1,10 @@ # GitLab Upgrader -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.* + +*DEPRECATED* We recommend to [switch to the Omnibus package and repository server](https://about.gitlab.com/update/) instead of using this script. + +Although deprecated, if someone wants to make this script into a gem or otherwise improve it merge requests are welcome. + +*Make sure you view this [upgrade guide from the 'master' branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.* GitLab Upgrader - a ruby script that allows you easily upgrade GitLab to latest minor version. @@ -24,14 +29,15 @@ If you have local changes to your GitLab repository the script will stash them a ## 2. Run GitLab upgrade tool -Note: GitLab 7.9 adds `nodejs` as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +Please replace X.X.X with the [latest GitLab release](https://packages.gitlab.com/gitlab/gitlab-ce). + +GitLab 7.9 adds `nodejs` as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) - # Starting with GitLab version 7.0 upgrader script has been moved to bin directory cd /home/git/gitlab - if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb; else sudo -u git -H ruby script/upgrade.rb; fi + sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' # to perform a non-interactive install (no user input required) you can add -y - # if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb -y; else sudo -u git -H ruby script/upgrade.rb -y; fi + # sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y ## 3. Start application @@ -60,17 +66,20 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` You've read through the entire guide and probably already did all the steps one by one. -Here is a one line command with step 1 to 5 for the next time you upgrade: +Below is a one line command with step 1 to 5 for the next time you upgrade. + +Please replace X.X.X with the [latest GitLab release](https://packages.gitlab.com/gitlab/gitlab-ce). ```bash cd /home/git/gitlab; \ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; \ sudo service gitlab stop; \ - if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb -y; else sudo -u git -H ruby script/upgrade.rb -y; fi; \ + sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y; \ cd /home/git/gitlab-shell; \ sudo -u git -H git fetch; \ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`; \ cd /home/git/gitlab; \ sudo service gitlab start; \ - sudo service nginx restart; sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production -``` + sudo service nginx restart; \ + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` \ No newline at end of file diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 851f50f5e9..73717ffc7d 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -12,6 +12,12 @@ If you send a web hook to an SSL endpoint [the certificate will not be verified] Triggered when you push to the repository except when pushing tags. +**Request header**: + +``` +X-Gitlab-Event: Push Hook +``` + **Request body:** ```json @@ -63,6 +69,13 @@ Triggered when you push to the repository except when pushing tags. Triggered when you create (or delete) tags to the repository. +**Request header**: + +``` +X-Gitlab-Event: Tag Push Hook +``` + + **Request body:** ```json @@ -92,6 +105,12 @@ Triggered when you create (or delete) tags to the repository. Triggered when a new issue is created or an existing issue was updated/closed/reopened. +**Request header**: + +``` +X-Gitlab-Event: Issue Hook +``` + **Request body:** ```json @@ -121,10 +140,295 @@ Triggered when a new issue is created or an existing issue was updated/closed/re } } ``` +## Comment events + +Triggered when a new comment is made on commits, merge requests, issues, and code snippets. +The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The +payload will also include information about the target of the comment. For example, +a comment on a issue will include the specific issue information under the `issue` key. +Valid target types: + +1. `commit` +2. `merge_request` +3. `issue` +4. `snippet` + +### Comment on commit + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Adminstrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://localhost/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1243, + "note": "This is a commit comment. How does this work?", + "noteable_type": "Commit", + "author_id": 1, + "created_at": "2015-05-17 18:08:09 UTC", + "updated_at": "2015-05-17 18:08:09 UTC", + "project_id": 5, + "attachment":null, + "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1", + "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "noteable_id": null, + "system": false, + "st_diff": { + "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n", + "new_path": "six", + "old_path": "six", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }, + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243" + }, + "commit": { + "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "timestamp": "2014-02-27T10:06:20+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } +} +``` + +### Comment on merge request + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1244, + "note": "This MR needs work.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2015-05-17 18:21:36 UTC", + "updated_at": "2015-05-17 18:21:36 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 7, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244" + }, + "merge_request": { + "id": 7, + "target_branch": "markdown", + "source_branch": "master", + "source_project_id": 5, + "author_id": 8, + "assignee_id": 28, + "title": "Tempora et eos debitis quae laborum et.", + "created_at": "2015-03-01 20:12:53 UTC", + "updated_at": "2015-03-21 18:27:27 UTC", + "milestone_id": 11, + "state": "opened", + "merge_status": "cannot_be_merged", + "target_project_id": 5, + "iid": 1, + "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", + "position": 0, + "locked_at": null, + "source": { + "name": "Gitlab Test", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10 + }, + "target": { + "name": "Gitlab Test", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10 + }, + "last_commit": { + "id": "562e173be03b8ff2efb05345d12df18815438a4b", + "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n", + "timestamp": "2015-04-08T21: 00:25-07:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b", + "author": { + "name": "John Smith", + "email": "john@example.com" + } + } + } +} +``` + +### Comment on issue + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Adminstrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 92, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_id": null, + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-12 14:53:17 UTC", + "updated_at": "2015-04-26 08:28:42 UTC", + "position": 0, + "branch_name": null, + "description": "test", + "milestone_id": null, + "state": "closed", + "iid": 17 + } +} +``` + +### Comment on code snippet + + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +``` +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1245, + "note": "Is this snippet doing what it's supposed to be doing?", + "noteable_type": "Snippet", + "author_id": 1, + "created_at": "2015-05-17 18:35:50 UTC", + "updated_at": "2015-05-17 18:35:50 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 53, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245" + }, + "snippet": { + "id": 53, + "title": "test", + "content": "puts 'Hello world'", + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-09 02:40:38 UTC", + "updated_at": "2015-04-09 02:40:38 UTC", + "file_name": "test.rb", + "expires_at": null, + "type": "ProjectSnippet", + "visibility_level": 0 + } +} +``` ## Merge request events -Triggered when a new merge request is created or an existing merge request was updated/merged/closed. +Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch. + +**Request header**: + +``` +X-Gitlab-Event: Merge Request Hook +``` **Request body:** diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 7e996dc47d..3915198ad2 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,15 +1,16 @@ # Workflow -- [Feature branch workflow](workflow.md) -- [Project forking workflow](forking_workflow.md) -- [Project Features](project_features.md) - [Authorization for merge requests](authorization_for_merge_requests.md) -- [Groups](groups.md) -- [Labels](labels.md) +- [Change your time zone](timezone.md) +- [Feature branch workflow](workflow.md) - [GitLab Flow](gitlab_flow.md) -- [Notifications](notifications.md) -- [Migrating from SVN to GitLab](migrating_from_svn.md) -- [Project importing from GitHub to GitLab](import_projects_from_github.md) -- [Project importing from GitLab.com to your private GitLab instance](import_projects_from_gitlab_com.md) +- [Groups](groups.md) +- [Keyboard shortcuts](shortcuts.md) +- [Labels](labels.md) +- [Notification emails](notifications.md) +- [Project Features](project_features.md) +- [Project forking workflow](forking_workflow.md) +- [Project users](add-user/add-user.md) - [Protected branches](protected_branches.md) - [Web Editor](web_editor.md) +- ["Work In Progress" Merge Requests](wip_merge_requests.md) diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md new file mode 100644 index 0000000000..8c9b4f7263 --- /dev/null +++ b/doc/workflow/add-user/add-user.md @@ -0,0 +1,25 @@ +# Project users + +You can manage the groups and users and their access levels in all of your projects. You can also personalize the access level you give each user, per project. + +Here's how to add or import users to your projects. + +You should have 'master' or 'owner' permissions to add or import a new user +to your project. + +To add or import a user, go to your project and click on "Members" on the left side of your screen: + +![Members](images/members.png) + +Select "Add members" or "Import members" on the right side of your screen: + +![Add or Import](images/add-members.png) + +If you are adding a user, select the user and the [permission level](doc/permissions/permissions.md) that you'd like to +give the user: + +![Add or Import](images/new-member.png) + +If you are importing a user, follow the steps to select the project where you'd like to import the user from: + +![Add or Import](images/select-project.png) diff --git a/doc/workflow/add-user/images/add-members.png b/doc/workflow/add-user/images/add-members.png new file mode 100644 index 0000000000..2805c5764a Binary files /dev/null and b/doc/workflow/add-user/images/add-members.png differ diff --git a/doc/workflow/add-user/images/members.png b/doc/workflow/add-user/images/members.png new file mode 100644 index 0000000000..f1797b95f6 Binary files /dev/null and b/doc/workflow/add-user/images/members.png differ diff --git a/doc/workflow/add-user/images/new-member.png b/doc/workflow/add-user/images/new-member.png new file mode 100644 index 0000000000..d500daea56 Binary files /dev/null and b/doc/workflow/add-user/images/new-member.png differ diff --git a/doc/workflow/add-user/images/select-project.png b/doc/workflow/add-user/images/select-project.png new file mode 100644 index 0000000000..dd3844edff Binary files /dev/null and b/doc/workflow/add-user/images/select-project.png differ diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 0e87dc7421..f608674faf 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -31,7 +31,7 @@ We think there is still room for improvement and will detail a set of practices ## Git flow and its problems -[![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png) +![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png) Git flow was one of the first proposals to use git branches and it has gotten a lot of attention. It advocates a master branch and a separate develop branch as well as supporting branches for features, releases and hotfixes. @@ -54,7 +54,7 @@ And doing releases doesn't automatically mean also doing hotfixes. ![Master branch with feature branches merged in](github_flow.png) - In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html). +In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html). This flow has only feature branches and a master branch. This is very simple and clean, many organizations have adopted it with great success. Atlassian recommends [a similar strategy](http://blogs.atlassian.com/2014/01/simple-git-workflow-simple/) although they rebase feature branches. @@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. If the assigned person does not feel comfortable they can close the merge request without merging. -In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/permissions/permissions.md). +In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html). So if you want to merge it into a protected branch you assign it to someone with master authorizations. ## Issues with GitLab flow @@ -216,7 +216,7 @@ This prevents creating a merge commit when merging master into your feature bran However, just like with squashing you should never rebase commits you have pushed to a remote server. This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend. When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](http://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/). -You can reuse recorded resolutions (rerere) sometimes, but with without rebasing you only have to solve the conflicts one time and you’re set. +You can reuse recorded resolutions (rerere) sometimes, but without rebasing you only have to solve the conflicts one time and you’re set. There has to be a better way to avoid many merge commits. The way to prevent creating many merge commits is to not frequently merge master into the feature branch. diff --git a/doc/workflow/import_projects_from_github.md b/doc/workflow/import_projects_from_github.md deleted file mode 100644 index 8644b4ffc7..0000000000 --- a/doc/workflow/import_projects_from_github.md +++ /dev/null @@ -1,13 +0,0 @@ -# Project importing from GitHub to GitLab - -You can import your existing GitHub projects to GitLab. But keep in mind that it is possible only if -GitHub support is enabled on your GitLab instance. You can read more about GitHub support [here](http://doc.gitlab.com/ce/integration/github.html) -To get to the importer page you need to go to "New project" page. - -![New project page](github_importer/new_project_page.png) - -Click on the "Import project from GitHub" link and you will be redirected to GitHub for permission to access your projects. After accepting, you'll be automatically redirected to the importer. - -![Importer page](github_importer/importer.png) - -To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data. \ No newline at end of file diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md new file mode 100644 index 0000000000..cd98d1b985 --- /dev/null +++ b/doc/workflow/importing/README.md @@ -0,0 +1,12 @@ +# Migrating projects to a GitLab instance + +1. [Bitbucket](import_projects_from_bitbucket.md) +2. [GitHub](import_projects_from_github.md) +3. [GitLab.com](import_projects_from_gitlab_com.md) +4. [SVN](migrating_from_svn.md) + +### Note +* If you'd like to migrate from a self-hosted GitLab instance to GitLab.com, you can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported. + +* Repositories are imported to GitLab via HTTP. +If the repository is too large, it can timeout. We have a soft limit of 10GB. diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png new file mode 100644 index 0000000000..df55a08180 Binary files /dev/null and b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png new file mode 100644 index 0000000000..5253889d25 Binary files /dev/null and b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png new file mode 100644 index 0000000000..ffa87ce5b2 Binary files /dev/null and b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png differ diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png new file mode 100644 index 0000000000..0e08703f42 Binary files /dev/null and b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png differ diff --git a/doc/workflow/github_importer/importer.png b/doc/workflow/importing/github_importer/importer.png similarity index 100% rename from doc/workflow/github_importer/importer.png rename to doc/workflow/importing/github_importer/importer.png diff --git a/doc/workflow/github_importer/new_project_page.png b/doc/workflow/importing/github_importer/new_project_page.png similarity index 100% rename from doc/workflow/github_importer/new_project_page.png rename to doc/workflow/importing/github_importer/new_project_page.png diff --git a/doc/workflow/gitlab_importer/importer.png b/doc/workflow/importing/gitlab_importer/importer.png similarity index 100% rename from doc/workflow/gitlab_importer/importer.png rename to doc/workflow/importing/gitlab_importer/importer.png diff --git a/doc/workflow/gitlab_importer/new_project_page.png b/doc/workflow/importing/gitlab_importer/new_project_page.png similarity index 100% rename from doc/workflow/gitlab_importer/new_project_page.png rename to doc/workflow/importing/gitlab_importer/new_project_page.png diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md new file mode 100644 index 0000000000..1e9825e2e1 --- /dev/null +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -0,0 +1,26 @@ +# Import your project from Bitbucket to GitLab + +It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](doc/integration/bitbucket.md). + +* Sign in to GitLab.com and go to your dashboard + +* Click on "New project" + +![New project in GitLab](bitbucket_importer/bitbucket_import_new_project.png) + +* Click on the "Bitbucket" button + +![Bitbucket](bitbucket_importer/bitbucket_import_select_bitbucket.png) + +* Grant GitLab access to your Bitbucket account + +![Grant access](bitbucket_importer/bitbucket_import_grant_access.png) + +* Click on the projects that you'd like to import or "Import all projects" + +![Import projects](bitbucket_importer/bitbucket_import_select_project.png) + +A new GitLab project will be created with your imported data. + +### Note +Milestones and wiki pages are not imported from Bitbucket. diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md new file mode 100644 index 0000000000..3efa92cb86 --- /dev/null +++ b/doc/workflow/importing/import_projects_from_github.md @@ -0,0 +1,20 @@ +# Import your project from GitHub to GitLab + +It takes just a couple of steps to import your existing GitHub projects to GitLab. Keep in mind that it is possible only if +GitHub support is enabled on your GitLab instance. You can read more about GitHub support [here](http://doc.gitlab.com/ce/integration/github.html) + +If you want to import from a GitHub Enterprise instance, you need to use GitLab Enterprise; please see the [EE docs for the GitHub integration](http://doc.gitlab.com/ee/integration/github.html). + +* Sign in to GitLab.com and go to your dashboard. +* To get to the importer page, you need to go to the "New project" page. + +![New project page](github_importer/new_project_page.png) + +* Click on the "Import project from GitHub" link and you will be redirected to GitHub for permission to access your projects. After accepting, you'll be automatically redirected to the importer. + +![Importer page](github_importer/importer.png) + +* To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data. + +### Note +When you import your projects from GitHub, it is not possible to keep your labels and milestones and issue numbers won't match. We are working on improving this in the near future. diff --git a/doc/workflow/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md similarity index 100% rename from doc/workflow/import_projects_from_gitlab_com.md rename to doc/workflow/importing/import_projects_from_gitlab_com.md diff --git a/doc/workflow/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md similarity index 100% rename from doc/workflow/migrating_from_svn.md rename to doc/workflow/importing/migrating_from_svn.md diff --git a/doc/workflow/labels.md b/doc/workflow/labels.md index 085b7baf5c..6e4840ca5a 100644 --- a/doc/workflow/labels.md +++ b/doc/workflow/labels.md @@ -1,6 +1,6 @@ # Labels -In GitLab, you can easily tag issues and merge requests. If you have permission level `Developer` or higher, you can manage labels. To create, edit or delete a label, go to a project and then to `Issues` and then `Labels`. +In GitLab, you can easily tag issues and Merge Requests. If you have permission level `Developer` or higher, you can manage labels. To create, edit or delete a label, go to a project and then to `Issues` and then `Labels`. Here you can create a new label. @@ -14,3 +14,5 @@ If you want to change an existing label, press edit next to the listed label. You will be presented with the same form as when creating a new label. ![edit label](labels/label3.png) + +You can add labels to Merge Requests when you create or edit them. diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 17215de677..80817c98d2 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -1,6 +1,6 @@ -# GitLab Notifications +# GitLab Notification Emails -GitLab has notifications system in place to notify a user of events important for the workflow. +GitLab has a notification system in place to notify a user of events that are important for the workflow. ## Notification settings @@ -52,20 +52,40 @@ Below is the table of events users can be notified of: | New SSH key added | User | Security email, always sent. | | New email added | User | Security email, always sent. | | New user created | User | Sent on user creation, except for omniauth (LDAP)| -| New issue created | Issue assignee [1], project members [2] | [1] not disabled, [2] higher than participating | | User added to project | User | Sent when user is added to project | | Project access level changed | User | Sent when user project access level is changed | | User added to group | User | Sent when user is added to group | +| Group access level changed | User | Sent when user group access level is changed | | Project moved | Project members [1] | [1] not disabled | -| Group access level changed | User | Sent when user group access level is changed | -| Close issue | Issue author [1], issue assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | -| Reassign issue | New issue assignee [1], old issue assignee [2] | [1] [2] not disabled | -| Reopen issue | Project members [1] | [1] higher than participating | -| New merge request | MR assignee [1] | [1] not disabled | -| Reassign merge request | New MR assignee [1], old MR assignee [2] | [1] [2] not disabled | -| Close merge request | MR author [1], MR assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | -| Reopen merge request | Project members [1] | [1] higher than participating | -| Merge merge request | MR author [1], MR assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | -| New comment | Mentioned users [1], users participating [2], project members [3] | [1] [2] not disabled, [3] higher than participating | +### Issue / Merge Request events +In all of the below cases, the notification will be sent to: +- Participants: + - the author and assignee of the issue/merge request + - authors of comments on the issue/merge request + - anyone mentioned by `@username` in the issue/merge request description + - anyone mentioned by `@username` in any of the comments on the issue/merge request + + ...with notification level "Participating" or higher + +- Watchers: project members with notification level "Watch" +- Subscribers: anyone who manually subscribed to the issue/merge request + +| Event | Sent to | +|------------------------|---------| +| New issue | | +| Close issue | | +| Reassign issue | The above, plus the old assignee | +| Reopen issue | | +| New merge request | | +| Reassign merge request | The above, plus the old assignee | +| Close merge request | | +| Reopen merge request | | +| Merge merge request | | +| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | + +You won't receive notifications for Issues, Merge Requests or Milestones +created by yourself. You will only receive automatic notifications when +somebody else comments or adds changes to the ones that you've created or +mentions you. diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md index 805f7f8d35..0adf9f8e3e 100644 --- a/doc/workflow/protected_branches.md +++ b/doc/workflow/protected_branches.md @@ -12,7 +12,7 @@ A protected branch does three simple things: You can make any branch a protected branch. GitLab makes the master branch a protected branch by default. -To protect a branch, user needs to have at least a Master permission level, see [permissions document](permissions/permissions.md). +To protect a branch, user needs to have at least a Master permission level, see [permissions document](doc/permissions/permissions.md). ![protected branches page](protected_branches/protected_branches1.png) @@ -28,6 +28,4 @@ For those workflows, you can allow everyone with write access to push to a prote On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box. -![Developers can push](protected_branches/protected_branches2.png) - - +![Developers can push](protected_branches/protected_branches2.png) \ No newline at end of file diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md new file mode 100644 index 0000000000..ffcb832cdd --- /dev/null +++ b/doc/workflow/shortcuts.md @@ -0,0 +1,5 @@ +# GitLab keyboard shortcuts + +You can see GitLab's keyboard shortcuts by using 'shift + ?' + +![Shortcuts](shortcuts.png) \ No newline at end of file diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png new file mode 100644 index 0000000000..68756ed1f9 Binary files /dev/null and b/doc/workflow/shortcuts.png differ diff --git a/doc/workflow/timezone.md b/doc/workflow/timezone.md new file mode 100644 index 0000000000..7e08c0e51a --- /dev/null +++ b/doc/workflow/timezone.md @@ -0,0 +1,30 @@ +# Changing your time zone + +The global time zone configuration parameter can be changed in `config/gitlab.yml`: +``` + # time_zone: 'UTC' +``` + +Uncomment and customize if you want to change the default time zone of GitLab application. + +To see all available time zones, run `bundle exec rake time:zones:all`. + + +## Changing time zone in omnibus installations + +GitLab defaults its time zone to UTC. It has a global timezone configuration parameter in `/etc/gitlab/gitlab.rb`. + +To update, add the time zone that best applies to your location. Here are two examples: +``` +gitlab_rails['time_zone'] = 'America/New_York' +``` +or +``` +gitlab_rails['time_zone'] = 'Europe/Brussels' +``` + +After you added this field, reconfigure and restart: +``` +gitlab-ctl reconfigure +gitlab-ctl restart +``` diff --git a/doc/workflow/wip_merge_requests.md b/doc/workflow/wip_merge_requests.md new file mode 100644 index 0000000000..46035a5e6b --- /dev/null +++ b/doc/workflow/wip_merge_requests.md @@ -0,0 +1,13 @@ +# "Work In Progress" Merge Requests + +To prevent merge requests from accidentally being accepted before they're completely ready, GitLab blocks the "Accept" button for merge requests that have been marked a **Work In Progress**. + +![Blocked Accept Button](wip_merge_requests/blocked_accept_button.png) + +To mark a merge request a Work In Progress, simply start its title with `[WIP]` or `WIP:`. + +![Mark as WIP](wip_merge_requests/mark_as_wip.png) + +To allow a Work In Progress merge request to be accepted again when it's ready, simply remove the `WIP` prefix. + +![Unark as WIP](wip_merge_requests/unmark_as_wip.png) diff --git a/doc/workflow/wip_merge_requests/blocked_accept_button.png b/doc/workflow/wip_merge_requests/blocked_accept_button.png new file mode 100644 index 0000000000..4791e5de97 Binary files /dev/null and b/doc/workflow/wip_merge_requests/blocked_accept_button.png differ diff --git a/doc/workflow/wip_merge_requests/mark_as_wip.png b/doc/workflow/wip_merge_requests/mark_as_wip.png new file mode 100644 index 0000000000..8fa83a201a Binary files /dev/null and b/doc/workflow/wip_merge_requests/mark_as_wip.png differ diff --git a/doc/workflow/wip_merge_requests/unmark_as_wip.png b/doc/workflow/wip_merge_requests/unmark_as_wip.png new file mode 100644 index 0000000000..d45e68f31c Binary files /dev/null and b/doc/workflow/wip_merge_requests/unmark_as_wip.png differ diff --git a/doc_styleguide.md b/doc_styleguide.md new file mode 100644 index 0000000000..656bb1d17f --- /dev/null +++ b/doc_styleguide.md @@ -0,0 +1,24 @@ +# Documentation styleguide + +This styleguide recommends best practices to improve documentation and to keep it organized and easy to find. + +## Text + +- Split up long lines, this makes it much easier to review and edit. Only +double line breaks are shown as a full line break in markdown. 80 characters +is a good line length. +- For subtitles, make sure to start with the largest and go down, meaning: +`#` for the title, `##` for subtitles and `###` for subtitles of the subtitles, etc. +- Make sure that the documentation is added in the correct directory and that there's a link to it somewhere useful. +- Add only one H1 or title in each document, by adding '#' at the begining of it (when using markdown). +For subtitles, use '##', '###' and so on. +- Do not duplicate information. +- Be brief and clear. +- Whenever it applies, add documents in alphabetical order. + +## Images + +- Create a directory to store the images with the specific name of the document where the images belong. +It could be in the same directory where the .md document that you're working on is located. +- Images should have a specific, non-generic name that will differentiate them. +- Keep all file names in lower case. \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index bb25bb677c..05521af696 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,19 +1,21 @@ FROM ubuntu:14.04 +MAINTAINER Sytse Sijbrandij # Install required packages RUN apt-get update -q \ - && DEBIAN_FRONTEND=noninteractive apt-get install -qy --no-install-recommends \ + && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ ca-certificates \ openssh-server \ - wget + wget \ + apt-transport-https \ + vim \ + nano # Download & Install GitLab -# If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. -RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.2-omnibus-1_amd64.deb \ - && dpkg -i $TMP_FILE \ - && rm -f $TMP_FILE +RUN echo "deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/gitlab_gitlab-ce.list +RUN wget -q -O - https://packages.gitlab.com/gpg.key | apt-key add - +RUN apt-get update && apt-get install -yq --no-install-recommends gitlab-ce # Manage SSHD through runit RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ @@ -23,8 +25,20 @@ RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ && mkdir -p /var/run/sshd +# Prepare default configuration +RUN ( \ + echo "" && \ + echo "# Docker options" && \ + echo "# Prevent Postgres from trying to allocate 25% of total memory" && \ + echo "postgresql['shared_buffers'] = '1MB'" ) >> /etc/gitlab/gitlab.rb && \ + mkdir -p /assets/ && \ + cp /etc/gitlab/gitlab.rb /assets/gitlab.rb + # Expose web & ssh -EXPOSE 80 22 +EXPOSE 443 80 22 + +# Define data volumes +VOLUME ["/etc/gitlab", "/var/opt/gitlab", "/var/log/gitlab"] # Copy assets COPY assets/wrapper /usr/local/bin/ diff --git a/docker/README.md b/docker/README.md index b7e8b0db7e..e4d56cdb33 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,88 +1,169 @@ -What is GitLab? -=============== +# GitLab Docker images -GitLab offers git repository management, code reviews, issue tracking, activity feeds, wikis. It has LDAP/AD integration, handles 25,000 users on a single server but can also run on a highly available active/active cluster. A subscription gives you access to our support team and to GitLab Enterprise Edition that contains extra features aimed at larger organizations. +The GitLab docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/). - +## After starting a container -![GitLab Logo](https://gitlab.com/uploads/appearance/logo/1/brand_logo-c37eb221b456bb4b472cc1084480991f.png) +After starting a container you can go to [http://localhost:8080/](http://localhost:8080/) or [http://192.168.59.103:8080/](http://192.168.59.103:8080/) if you use boot2docker. +It might take a while before the docker container is responding to queries. -How to use these images -====================== +You can check the status with something like `sudo docker logs -f gitlab`. -At this moment GitLab doesn't have official Docker images. For convinience we will use suffix _xy where xy is current version of GitLab. -Build your own based on the Omnibus packages with the following commands (it assumes you're in the GitLab repo root directory): +You can login to the web interface with username `root` and password `5iveL!fe`. +Next time, you can just use docker start and stop to run the container. + +## Run the image + +Run the image: ```bash -sudo docker build --tag gitlab_data_image docker/data/ -sudo docker build --tag gitlab_app_image_xy docker/ +sudo docker run --detach \ + --publish 8443:443 --publish 8080:80 --publish 2222:22 \ + --name gitlab \ + --restart always \ + --volume /srv/gitlab/config:/etc/gitlab \ + --volume /srv/gitlab/logs:/var/log/gitlab \ + --volume /srv/gitlab/data:/var/opt/gitlab \ + gitlab/gitlab-ce:latest ``` -We assume using a data volume container, this will simplify migrations and backups. -This empty container will exist to persist as volumes the 3 directories used by GitLab, so remember not to delete it. +This will download and start GitLab CE container and publish ports needed to access SSH, HTTP and HTTPS. +All GitLab data will be stored as subdirectories of `/srv/gitlab/`. +The container will automatically `restart` after system reboot. -The directories on data container are: +After this you can login to the web interface as explained above in 'After starting a container'. -- `/var/opt/gitlab` for application data -- `/var/log/gitlab` for logs -- `/etc/gitlab` for configuration +## Where is the data stored? -Create the data container with: +The GitLab container uses host mounted volumes to store persistent data: +- `/srv/gitlab/data` mounted as `/var/opt/gitlab` in the container is used for storing *application data* +- `/srv/gitlab/logs` mounted as `/var/log/gitlab` in the container is used for storing *logs* +- `/srv/gitlab/config` mounted as `/etc/gitlab` in the container is used for storing *configuration* -```bash -sudo docker run --name gitlab_data gitlab_data_image /bin/true -``` +You can fine tune these directories to meet your requirements. -After creating data container run GitLab container: - -```bash -sudo docker run --detach --name gitlab_app_xy --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_app_image_xy -``` - -It might take a while before the docker container is responding to queries. You can follow the configuration process with `sudo docker logs -f gitlab_app_xy`. - -You can then go to `http://localhost:8080/` (or `http://192.168.59.103:8080/` if you use boot2docker). -You can login with username `root` and password `5iveL!fe`. -Next time, you can just use `sudo docker start gitlab_app` and `sudo docker stop gitlab_app`. - - -How to configure GitLab -======================== +### Configure GitLab This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. -To access GitLab configuration, you can start an interactive command line in a new container using the shared data volume container, you will be able to browse the 3 directories and use your favorite text editor: - +To access GitLab configuration, you can start an bash in a new the context of running container, you will be able to browse all directories and use your favorite text editor: ```bash -sudo docker run -ti -e TERM=linux --rm --volumes-from gitlab_data ubuntu -vi /etc/gitlab/gitlab.rb +sudo docker exec -it gitlab /bin/bash ``` -**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab. - -You can find all available options in [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). - -How to upgrade GitLab -======================== - -To updgrade GitLab to new versions, stop running container, create new docker image and container from that image. - -It Assumes that you're upgrading from 7.8 to 7.9 and you're in the updated GitLab repo root directory: - +You can also edit just `/etc/gitlab/gitlab.rb`: ```bash -sudo docker stop gitlab_app_78 -sudo docker build --tag gitlab_app_image_79 docker/ -sudo docker run --detach --name gitlab_app_79 --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_app_image_79 +sudo docker exec -it gitlab vi /etc/gitlab/gitlab.rb ``` -On the first run GitLab will reconfigure and update itself. If everything runs OK don't forget to cleanup old container and image: +**You should set the `external_url` to point to a valid URL.** + +**You may also be interesting in [Enabling HTTPS](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md#enable-https).** + +**To receive e-mails from GitLab you have to configure the [SMTP settings](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/smtp.md), +because Docker image doesn't have a SMTP server.** + +**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab: ```bash -sudo docker rm gitlab_app_78 -sudo docker rmi gitlab_app_image_78 +sudo docker restart gitlab ``` -Troubleshooting -========================= +For more options for configuring the container please check [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). + +## Diagnose potential problems + +Read container logs: +```bash +sudo docker logs gitlab +``` + +Enter running container: +```bash +sudo docker exec -it gitlab /bin/bash +``` + +From within container you can administrer GitLab container as you would normally administer Omnibus installation: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md. + +### Upgrade GitLab to newer version + +To upgrade GitLab to new version you have to do: +1. pull new image, +```bash +sudo docker stop gitlab +``` + +1. stop running container, +```bash +sudo docker rm gitlab +``` + +1. remove existing container, +```bash +sudo docker pull gitlab/gitlab-ce:latest +``` + +1. create the container once again with previously specified options. +```bash +sudo docker run --detach \ + --publish 8443:443 --publish 8080:80 --publish 2222:22 \ + --name gitlab \ + --restart always \ + --volume /srv/gitlab/config:/etc/gitlab \ + --volume /srv/gitlab/logs:/var/log/gitlab \ + --volume /srv/gitlab/data:/var/opt/gitlab \ + gitlab/gitlab-ce:latest +``` + +On the first run GitLab will reconfigure and update itself. + +### Run GitLab CE on public IP address + +You can make Docker to use your IP address and forward all traffic to the GitLab CE container. +You can do that by modifying the `--publish` ([Binding container ports to the host](https://docs.docker.com/articles/networking/#binding-ports)): + +> --publish=[] : Publish a container᾿s port or a range of ports to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + +To expose GitLab CE on IP 1.1.1.1: + +```bash +sudo docker run --detach \ + --publish 1.1.1.1:443:443 --publish 1.1.1.1:80:80 --publish 1.1.1.1:22:22 \ + --name gitlab \ + --restart always \ + --volume /srv/gitlab/config:/etc/gitlab \ + --volume /srv/gitlab/logs:/var/log/gitlab \ + --volume /srv/gitlab/data:/var/opt/gitlab \ + gitlab/gitlab-ce:latest +``` + +You can then access GitLab instance at http://1.1.1.1/ and https://1.1.1.1/. + +### Build the image + +This guide will also let you know how to build docker image yourself. +Please run the command from the GitLab repo root directory. +People using boot2docker should run all the commands without sudo. + +```bash +sudo docker build --tag gitlab/gitlab-ce:latest docker/ +``` + +### Publish the image to Dockerhub + +- Ensure the containers are running +- Login to Dockerhub with `sudo docker login` + +```bash +sudo docker login +sudo docker push gitlab/gitlab-ce:latest +``` + +## Troubleshooting + Please see the [troubleshooting](troubleshooting.md) file in this directory. + +Note: We use `fig.yml` to have compatibility with fig and because docker-compose also supports it. + +Our docker image runs chef at every start to generate GitLab configuration. diff --git a/docker/assets/wrapper b/docker/assets/wrapper index 9e6e7a0590..8bc8370fbc 100755 --- a/docker/assets/wrapper +++ b/docker/assets/wrapper @@ -8,10 +8,14 @@ function sigterm_handler() { trap "sigterm_handler; exit" TERM function entrypoint() { - # Default is to run runit and reconfigure GitLab - gitlab-ctl reconfigure & /opt/gitlab/embedded/bin/runsvdir-start & - wait + gitlab-ctl reconfigure # will also start everything + gitlab-ctl tail # tail all logs } +if [[ ! -e /etc/gitlab/gitlab.rb ]]; then + cp /assets/gitlab.rb /etc/gitlab/gitlab.rb + chmod 0600 /etc/gitlab/gitlab.rb +fi + entrypoint diff --git a/docker/data/Dockerfile b/docker/data/Dockerfile deleted file mode 100644 index ea0175c4aa..0000000000 --- a/docker/data/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM busybox - -# Declare volumes -VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] -# Copy assets -COPY assets/gitlab.rb /etc/gitlab/ - -CMD /bin/sh diff --git a/docker/data/assets/gitlab.rb b/docker/data/assets/gitlab.rb deleted file mode 100644 index 7fddf309c0..0000000000 --- a/docker/data/assets/gitlab.rb +++ /dev/null @@ -1,37 +0,0 @@ -# External URL should be your Docker instance. -# By default, this example is the "standard" boot2docker IP. -# Always use port 80 here to force the internal nginx to bind port 80, -# even if you intend to use another port in Docker. -external_url "http://192.168.59.103/" - -# Prevent Postgres from trying to allocate 25% of total memory -postgresql['shared_buffers'] = '1MB' - -# Configure GitLab to redirect PostgreSQL logs to the data volume -postgresql['log_directory'] = '/var/log/gitlab/postgresql' - -# Some configuration of GitLab -# You can find more at https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration -gitlab_rails['gitlab_email_from'] = 'gitlab@example.com' -gitlab_rails['gitlab_support_email'] = 'support@example.com' -gitlab_rails['time_zone'] = 'Europe/Paris' - -# SMTP settings -# You must use an external server, the Docker container does not install an SMTP server -gitlab_rails['smtp_enable'] = true -gitlab_rails['smtp_address'] = "smtp.example.com" -gitlab_rails['smtp_port'] = 587 -gitlab_rails['smtp_user_name'] = "user" -gitlab_rails['smtp_password'] = "password" -gitlab_rails['smtp_domain'] = "example.com" -gitlab_rails['smtp_authentication'] = "plain" -gitlab_rails['smtp_enable_starttls_auto'] = true - -# Enable LDAP authentication -# gitlab_rails['ldap_enabled'] = true -# gitlab_rails['ldap_host'] = 'ldap.example.com' -# gitlab_rails['ldap_port'] = 389 -# gitlab_rails['ldap_method'] = 'plain' # 'ssl' or 'plain' -# gitlab_rails['ldap_allow_username_or_email_login'] = false -# gitlab_rails['ldap_uid'] = 'uid' -# gitlab_rails['ldap_base'] = 'ou=users,dc=example,dc=com' diff --git a/docker/fig.yml b/docker/fig.yml new file mode 100644 index 0000000000..989551cbfe --- /dev/null +++ b/docker/fig.yml @@ -0,0 +1,2 @@ +app: + build: . diff --git a/docker/marathon.json b/docker/marathon.json new file mode 100644 index 0000000000..9b2091a8c2 --- /dev/null +++ b/docker/marathon.json @@ -0,0 +1,31 @@ +{ + "id": "/gitlab", + "ports": [0,0], + "cpus": 2, + "mem": 2048.0, + "disk": 10240.0, + "container": { + "type": "DOCKER", + "docker": { + "network": "HOST", + "image": "gitlab/gitlab-ce:latest" + }, + "volumes": [ + { + "containerPath": "/etc/gitlab", + "hostPath": "/var/data/etc/gitlab", + "mode": "RW" + }, + { + "containerPath": "/var/opt/gitlab", + "hostPath": "/var/data/opt/gitlab", + "mode": "RW" + }, + { + "containerPath": "/var/log/gitlab", + "hostPath": "/var/data/log/gitlab", + "mode": "RW" + } + ] + } +} \ No newline at end of file diff --git a/docker/troubleshooting.md b/docker/troubleshooting.md index b1b70de599..63482547da 100644 --- a/docker/troubleshooting.md +++ b/docker/troubleshooting.md @@ -9,24 +9,19 @@ postgresql['log_directory'] = '/var/log/gitlab/postgresql' # Commands ```bash -sudo docker build --tag gitlab_image docker/ +sudo docker build --tag gitlab/gitlab-ce:latest docker/ -sudo docker rm -f gitlab_app -sudo docker rm -f gitlab_data +sudo docker rm -f gitlab -sudo docker run --name gitlab_data gitlab_image /bin/true +sudo docker exec -it gitlab vim /etc/gitlab/gitlab.rb -sudo docker run -ti --rm --volumes-from gitlab_data ubuntu apt-get update && sudo apt-get install -y vim && sudo vim /etc/gitlab/gitlab.rb +sudo docker exec gitlab tail -f /var/log/gitlab/reconfigure.log -sudo docker run --detach --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image +sudo docker exec gitlab tail -f /var/log/gitlab/postgresql/current -sudo docker run -t --rm --volumes-from gitlab_data ubuntu tail -f /var/log/gitlab/reconfigure.log +sudo docker exec gitlab cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers -sudo docker run -t --rm --volumes-from gitlab_data ubuntu tail -f /var/log/gitlab/postgresql/current - -sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers - -sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /etc/gitlab/gitlab.rb +sudo docker exec gitlab cat /etc/gitlab/gitlab.rb ``` # Interactively @@ -37,7 +32,16 @@ sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /etc/gitlab/gitlab # - we run interactively (-t -i) # - we define TERM=linux because it allows to use arrow keys in vi (!!!) # - we choose another startup command (bash) -sudo docker run -ti -e TERM=linux --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image bash +sudo docker run --ti \ + -e TERM=linux + --publish 80443:443 --publish 8080:80 --publish 2222:22 \ + --name gitlab \ + --restart always \ + --volume /srv/gitlab/config:/etc/gitlab \ + --volume /srv/gitlab/logs:/var/log/gitlab \ + --volume /srv/gitlab/data:/var/opt/gitlab \ + gitlab/gitlab-ce:latest \ + bash # Configure GitLab to redirect PostgreSQL logs echo "postgresql['log_directory'] = '/var/log/gitlab/postgresql'" >> /etc/gitlab/gitlab.rb @@ -61,3 +65,20 @@ head /proc/sys/kernel/shmmax /proc/sys/kernel/shmall free -m ``` + +# Cleanup + +Remove ALL docker containers and images (also non GitLab ones). +**Be careful, because the `-v` also removes volumes attached to the images.** + +```bash +# Remove all containers with attached volumes +docker rm -v $(docker ps -a -q) + +# Remove all images +docker rmi $(docker images -q) + +# Remove GitLab persistent data +rm -rf /srv/gitlab +``` + diff --git a/features/abuse_report.feature b/features/abuse_report.feature new file mode 100644 index 0000000000..3e1cb455b7 --- /dev/null +++ b/features/abuse_report.feature @@ -0,0 +1,10 @@ +Feature: Abuse reports + Background: + Given I sign in as a user + And user "Mike" exists + + Scenario: Report abuse + Given I visit "Mike" user page + And I click "Report abuse" button + When I fill and submit abuse form + Then I should see success message diff --git a/features/admin/abuse_report.feature b/features/admin/abuse_report.feature new file mode 100644 index 0000000000..7d4ec2556e --- /dev/null +++ b/features/admin/abuse_report.feature @@ -0,0 +1,8 @@ +Feature: Admin Abuse reports + Background: + Given I sign in as an admin + And abuse reports exist + + Scenario: Browse abuse reports + When I visit abuse reports page + Then I should see list of abuse reports diff --git a/features/admin/deploy_keys.feature b/features/admin/deploy_keys.feature index 9df47eb51f..33439cd1e8 100644 --- a/features/admin/deploy_keys.feature +++ b/features/admin/deploy_keys.feature @@ -8,11 +8,6 @@ Feature: Admin Deploy Keys When I visit admin deploy keys page Then I should see all public deploy keys - Scenario: Deploy Keys show - When I visit admin deploy keys page - And I click on first deploy key - Then I should see deploy key details - Scenario: Deploy Keys new When I visit admin deploy keys page And I click 'New Deploy Key' diff --git a/features/admin/groups.feature b/features/admin/groups.feature index aa365a6ea1..973918086a 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -27,3 +27,9 @@ Feature: Admin Groups When I visit admin group page And I remove user "John Doe" from group Then I should not see "John Doe" in team list + + @javascript + Scenario: Invite user to a group by e-mail + When I visit admin group page + When I select user "johndoe@gitlab.com" from user list as "Reporter" + Then I should see "johndoe@gitlab.com" in team list in every project as "Reporter" diff --git a/features/admin/settings.feature b/features/admin/settings.feature index 52e47307b2..e38eea2cfe 100644 --- a/features/admin/settings.feature +++ b/features/admin/settings.feature @@ -11,6 +11,9 @@ Feature: Admin Settings Scenario: Change Slack Service Template settings When I click on "Service Templates" And I click on "Slack" service + And I fill out Slack settings Then I check all events and submit form And I should see service template settings saved + Then I click on "Slack" service And I should see all checkboxes checked + And I should see Slack settings saved diff --git a/features/admin/users.feature b/features/admin/users.feature index 1a8720dd77..6755645778 100644 --- a/features/admin/users.feature +++ b/features/admin/users.feature @@ -28,7 +28,7 @@ Feature: Admin Users When I submit modified user Then I see user attributes changed -@javascript + @javascript Scenario: Remove users secondary email Given I visit admin users page And I view the user with secondary email @@ -40,8 +40,26 @@ Feature: Admin Users Given user "Pete" with ssh keys And I visit admin users page And click on user "Pete" + And click on ssh keys tab Then I should see key list And I click on the key title Then I should see key details And I click on remove key Then I should see the key removed + + Scenario: Show user identities + Given user "Pete" with twitter account + And I visit "Pete" identities page in admin + Then I should see twitter details + + Scenario: Update user identities + Given user "Pete" with twitter account + And I visit "Pete" identities page in admin + And I modify twitter identity + Then I should see twitter details updated + + Scenario: Remove user identities + Given user "Pete" with twitter account + And I visit "Pete" identities page in admin + And I remove twitter identity + Then I should not see twitter details diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature index cf4b8d7283..e3c01db2eb 100644 --- a/features/dashboard/group.feature +++ b/features/dashboard/group.feature @@ -24,7 +24,8 @@ Feature: Dashboard Group When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list - Then I should not see the "Leave" button for group "Owned" + When I click on the "Leave" button for group "Owned" + Then I should see the "Can not leave message" @javascript Scenario: Guest should be able to leave from group diff --git a/features/groups.feature b/features/groups.feature index 415e43d6ae..299e846edb 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -4,6 +4,10 @@ Feature: Groups And "John Doe" is owner of group "Owned" And "John Doe" is guest of group "Guest" + Scenario: I should have back to group button + When I visit group "Owned" page + Then I should see back to dashboard button + @javascript Scenario: I should see group "Owned" dashboard list When I visit group "Owned" page diff --git a/features/profile/active_tab.feature b/features/profile/active_tab.feature index 7801ae5b8c..788b7895d7 100644 --- a/features/profile/active_tab.feature +++ b/features/profile/active_tab.feature @@ -18,12 +18,12 @@ Feature: Profile Active Tab Then the active main tab should be SSH Keys And no other main tabs should be active - Scenario: On Profile Design - Given I visit profile design page - Then the active main tab should be Design + Scenario: On Profile Preferences + Given I visit profile preferences page + Then the active main tab should be Preferences And no other main tabs should be active - Scenario: On Profile History - Given I visit profile history page - Then the active main tab should be History + Scenario: On Profile Audit Log + Given I visit Audit Log page + Then the active main tab should be Audit Log And no other main tabs should be active diff --git a/features/profile/profile.feature b/features/profile/profile.feature index d586167cdf..27c0bde364 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -35,6 +35,7 @@ Feature: Profile Then I change my avatar And I should see new avatar And I should see the "Remove avatar" button + And I should see the gravatar host link Scenario: I remove my avatar Given I visit profile page @@ -42,6 +43,7 @@ Feature: Profile When I remove my avatar Then I should see my gravatar And I should not see the "Remove avatar" button + And I should see the gravatar host link Scenario: My password is expired Given my password is expired @@ -63,7 +65,7 @@ Feature: Profile Scenario: I visit history tab Given I have activity - When I visit profile history page + When I visit Audit Log page Then I should see my activity Scenario: I visit my user page @@ -84,16 +86,3 @@ Feature: Profile Then I visit profile applications page And I click to remove application Then I see that application is removed - - @javascript - Scenario: I change my application theme - Given I visit profile design page - When I change my application theme - Then I should see the theme change immediately - And I should receive feedback that the changes were saved - - @javascript - Scenario: I change my code preview theme - Given I visit profile design page - When I change my code preview theme - Then I should receive feedback that the changes were saved diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 05faad4e64..8661ea98c2 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -35,6 +35,11 @@ Feature: Project Active Tab Then the active main tab should be Merge Requests And no other main tabs should be active + Scenario: On Project Members + Given I visit my project's members page + Then the active main tab should be Members + And no other main tabs should be active + Scenario: On Project Wiki Given I visit my project's wiki page Then the active main tab should be Wiki @@ -49,13 +54,6 @@ Feature: Project Active Tab # Sub Tabs: Settings - Scenario: On Project Settings/Team - Given I visit my project's settings page - And I click the "Team" tab - Then the active sub nav should be Team - And no other sub navs should be active - And the active main tab should be Settings - Scenario: On Project Settings/Edit Given I visit my project's settings page And I click the "Edit" tab diff --git a/features/project/commits/comments.feature b/features/project/commits/comments.feature index c41075d7ad..320f008abb 100644 --- a/features/project/commits/comments.feature +++ b/features/project/commits/comments.feature @@ -39,6 +39,7 @@ Feature: Project Commits Comments @javascript Scenario: I can delete a comment Given I leave a comment like "XML attached" + Then I should see a comment saying "XML attached" And I delete a comment Then I should not see a comment saying "XML attached" diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index c4b206edc9..3ebc8a39aa 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -41,6 +41,7 @@ Feature: Project Commits Scenario: I browse big commit Given I visit big commit page Then I see big commit warning + And I see "Reload with full diff" link Scenario: I browse a commit with an image Given I visit a commit with an image that changed diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature index 56b9a13678..4a2b870e08 100644 --- a/features/project/commits/diff_comments.feature +++ b/features/project/commits/diff_comments.feature @@ -77,3 +77,17 @@ Feature: Project Commits Diff Comments And I submit the diff comment Then I should not see the diff comment form And I should see a discussion reply button + + @javascript + Scenario: I can add a comment on a side-by-side commit diff (left side) + Given I open a diff comment form + And I click side-by-side diff button + When I leave a diff comment in a parallel view on the left side like "Old comment" + Then I should see a diff comment on the left side saying "Old comment" + + @javascript + Scenario: I can add a comment on a side-by-side commit diff (right side) + Given I open a diff comment form + And I click side-by-side diff button + When I leave a diff comment in a parallel view on the right side like "New comment" + Then I should see a diff comment on the right side saying "New comment" diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature index a71f6124d9..47cf774094 100644 --- a/features/project/deploy_keys.feature +++ b/features/project/deploy_keys.feature @@ -9,9 +9,10 @@ Feature: Project Deploy Keys Then I should see project deploy key Scenario: I should see project deploy keys - Given other project has deploy key + Given other projects have deploy keys When I visit project deploy keys page - Then I should see other project deploy key + Then I should see other project deploy key + And I should only see the same deploy key once Scenario: I should see public deploy keys Given public deploy key exists @@ -26,7 +27,7 @@ Feature: Project Deploy Keys And I should see newly created deploy key Scenario: I attach other project deploy key to project - Given other project has deploy key + Given other projects have deploy keys And I visit project deploy keys page When I click attach deploy key Then I should be on deploy keys page diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature index d9fbb875c2..10bd6fec80 100644 --- a/features/project/forked_merge_requests.feature +++ b/features/project/forked_merge_requests.feature @@ -26,7 +26,6 @@ Feature: Project Forked Merge Requests #And I save the merge request #Then I should see the edited merge request - @javascript Scenario: I cannot submit an invalid merge request Given I visit project "Forked Shop" merge requests page And I click link "New Merge Request" @@ -38,3 +37,15 @@ Feature: Project Forked Merge Requests Given I visit project "Forked Shop" merge requests page And I click link "New Merge Request" Then the target repository should be the original repository + + @javascript + Scenario: I see the users in the target project for a new merge request + Given I logout + And I sign in as an admin + And I have a project forked off of "Shop" called "Forked Shop" + Then I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out a "Merge Request On Forked Project" merge request + When I click "Assign to" dropdown" + Then I should see the target project ID in the input selector + And I should see the users from the target project ID diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index eb813884d1..28cc43ef71 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -134,48 +134,15 @@ Feature: Project Issues And I should see "Release 0.4" in issues And I should not see "Tweet control" in issues - Scenario: Issue description should render task checkboxes - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit issue page "Tasks-open" - Then I should see task checkboxes in the description - - @javascript - Scenario: Issue notes should not render task checkboxes - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit issue page "Tasks-open" - And I leave a comment with task markdown - Then I should not see task checkboxes in the comment - @javascript Scenario: Issue notes should be editable with +1 - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit issue page "Tasks-open" + Given project "Shop" have "Release 0.4" open issue + When I visit issue page "Release 0.4" And I leave a comment with a header containing "Comment with a header" Then The comment with the header should not have an ID And I edit the last comment with a +1 Then I should see +1 in the description - # Task status in issues list - - Scenario: Issues list should display task status - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit project "Shop" issues page - Then I should see the task status for the Taskable - - # Toggling task items - - @javascript - Scenario: Task checkboxes should be enabled for an open issue - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit issue page "Tasks-open" - Then Task checkboxes should be enabled - - @javascript - Scenario: Task checkboxes should be disabled for a closed issue - Given project "Shop" has "Tasks-closed" closed issue with task markdown - When I visit issue page "Tasks-closed" - Then Task checkboxes should be disabled - # Issue description preview @javascript @@ -212,8 +179,21 @@ Feature: Project Issues @javascript Scenario: I can unsubscribe from issue - Given project "Shop" has "Tasks-open" open issue with task markdown - When I visit issue page "Tasks-open" + Given project "Shop" have "Release 0.4" open issue + When I visit issue page "Release 0.4" Then I should see that I am subscribed When I click button "Unsubscribe" Then I should see that I am unsubscribed + + Scenario: I submit new unassigned issue as guest + Given I logout + Given public project "Community" + When I visit project "Community" page + And I visit project "Community" issues page + And I click link "New Issue" + And I should not see assignee field + And I should not see milestone field + And I should not see labels field + And I submit new issue "500 error on profile" + Then I should see issue "500 error on profile" + diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature index 9ac65b1257..bfbaaec5a3 100644 --- a/features/project/issues/milestones.feature +++ b/features/project/issues/milestones.feature @@ -17,6 +17,10 @@ Feature: Project Issues Milestones And I submit new milestone "v2.3" Then I should see milestone "v2.3" + Scenario: I delete new milestone + Given I click link to remove milestone "v2.2" + And I should see no milestones + @javascript Scenario: Listing closed issues Given the milestone has open and closed issues diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index cbb5c8eb39..947f668e43 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -10,7 +10,7 @@ Feature: Project Merge Requests Then I should see "Bug NS-04" in merge requests And I should not see "Feature NS-03" in merge requests - Scenario: I should see closed merge requests + Scenario: I should see rejected merge requests Given I click link "Closed" Then I should see "Feature NS-03" in merge requests And I should not see "Bug NS-04" in merge requests @@ -41,6 +41,18 @@ Feature: Project Merge Requests And I submit new merge request "Wiki Feature" Then I should see merge request "Wiki Feature" + Scenario: I download a diff on a public merge request + Given public project "Community" + And "John Doe" owns public project "Community" + And project "Community" has "Bug CO-01" open merge request with diffs inside + Given I logout directly + And I visit merge request page "Bug CO-01" + And I click on "Email Patches" + Then I should see a patch diff + And I visit merge request page "Bug CO-01" + And I click on "Plain Diff" + Then I should see a patch diff + @javascript Scenario: I comment on a merge request Given I visit merge request page "Bug NS-04" @@ -51,7 +63,7 @@ Feature: Project Merge Requests Scenario: I comment on a merge request diff Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" - And I switch to the diff tab + And I click on the Changes tab And I leave a comment like "Line is wrong" on diff And I switch to the merge request's comments tab Then I should see a discussion has started on diff @@ -96,23 +108,13 @@ Feature: Project Merge Requests And I leave a comment with a header containing "Comment with a header" Then The comment with the header should not have an ID - Scenario: Merge request description should render task checkboxes - Given project "Shop" has "MR-task-open" open MR with task markdown - When I visit merge request page "MR-task-open" - Then I should see task checkboxes in the description - - Scenario: Merge request notes should not render task checkboxes - Given project "Shop" has "MR-task-open" open MR with task markdown - When I visit merge request page "MR-task-open" - Then I should not see task checkboxes in the comment - # Toggling inline comments @javascript Scenario: I hide comments on a merge request diff with comments in a single file Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" - And I switch to the diff tab + And I click on the Changes tab And I leave a comment like "Line is wrong" on line 39 of the second file And I click link "Hide inline discussion" of the second file Then I should not see a comment like "Line is wrong here" in the second file @@ -121,7 +123,7 @@ Feature: Project Merge Requests Scenario: I show comments on a merge request diff with comments in a single file Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" - And I switch to the diff tab + And I click on the Changes tab And I leave a comment like "Line is wrong" on line 39 of the second file Then I should see a comment like "Line is wrong" in the second file @@ -129,7 +131,7 @@ Feature: Project Merge Requests Scenario: I hide comments on a merge request diff with comments in multiple files Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" - And I switch to the diff tab + And I click on the Changes tab And I leave a comment like "Line is correct" on line 12 of the first file And I leave a comment like "Line is wrong" on line 39 of the second file And I click link "Hide inline discussion" of the second file @@ -140,7 +142,7 @@ Feature: Project Merge Requests Scenario: I show comments on a merge request diff with comments in multiple files Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" - And I switch to the diff tab + And I click on the Changes tab And I leave a comment like "Line is correct" on line 12 of the first file And I leave a comment like "Line is wrong" on line 39 of the second file And I click link "Hide inline discussion" of the second file @@ -152,7 +154,7 @@ Feature: Project Merge Requests Scenario: I unfold diff Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" - And I switch to the diff tab + And I click on the Changes tab And I unfold diff Then I should see additional file lines @@ -160,7 +162,7 @@ Feature: Project Merge Requests Scenario: I show comments on a merge request side-by-side diff with comments in multiple files Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" - And I switch to the diff tab + And I click on the Changes tab And I leave a comment like "Line is correct" on line 12 of the first file And I leave a comment like "Line is wrong" on line 39 of the second file And I click Side-by-side Diff tab @@ -170,31 +172,9 @@ Feature: Project Merge Requests Scenario: I view diffs on a merge request Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" - And I click on the Changes tab via Javascript + And I click on the Changes tab Then I should see the proper Inline and Side-by-side links - # Task status in issues list - - Scenario: Merge requests list should display task status - Given project "Shop" has "MR-task-open" open MR with task markdown - When I visit project "Shop" merge requests page - Then I should see the task status for the Taskable - - # Toggling task items - - @javascript - Scenario: Task checkboxes should be enabled for an open merge request - Given project "Shop" has "MR-task-open" open MR with task markdown - When I visit merge request page "MR-task-open" - Then Task checkboxes should be enabled - - @javascript - Scenario: Task checkboxes should be disabled for a closed merge request - Given project "Shop" has "MR-task-open" open MR with task markdown - And I visit merge request page "MR-task-open" - And I click link "Close" - Then Task checkboxes should be disabled - # Description preview @javascript @@ -239,3 +219,11 @@ Feature: Project Merge Requests Then I should see that I am subscribed When I click button "Unsubscribe" Then I should see that I am unsubscribed + + @javascript + Scenario: I can change the target branch + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + When I click the "Target branch" dropdown + And I select a new target branch + Then I should see new target branch changes diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature index 8beb6043af..6cc89a15a7 100644 --- a/features/project/network_graph.feature +++ b/features/project/network_graph.feature @@ -10,6 +10,11 @@ Feature: Project Network Graph And page should select "master" in select box And page should have "master" on graph + @javascript + Scenario: I should see project network with 'test' branch + When I visit project network page on branch 'test' + Then page should have 'test' on graph + @javascript Scenario: I should switch "branch" and "tag" When I switch ref to "feature" diff --git a/features/project/project.feature b/features/project/project.feature index 3e1fd54bee..089ffcba14 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -18,9 +18,22 @@ Feature: Project Then I should see the default project avatar And I should not see the "Remove avatar" button + Scenario: I should have back to group button + And project "Shop" belongs to group + And I visit project "Shop" page + Then I should see back to group button + + Scenario: I should have back to group button + And I visit project "Shop" page + Then I should see back to dashboard button + + Scenario: I should have readme on page + And I visit project "Shop" page + Then I should see project "Shop" README + @javascript Scenario: I should see project activity - When I visit project "Shop" page + When I visit project "Shop" activity page Then I should see project "Shop" activity feed Scenario: I visit edit project @@ -38,20 +51,26 @@ Feature: Project And change project path settings Then I should see project with new path settings - Scenario: I should see project readme and version - When I visit project "Shop" page - And I should see project "Shop" version - Scenario: I should change project default branch When I visit edit project "Shop" page And change project default branch And I save project Then I should see project default branch changed - @javascript - Scenario: I should have default tab per my preference - And I own project "Forum" - When I select project "Forum" README tab - Then I should see project "Forum" README - And I visit project "Shop" page - Then I should see project "Shop" README + Scenario: I tag a project + When I visit edit project "Shop" page + Then I should see project settings + And I add project tags + And I save project + Then I should see project tags + + Scenario: I should not see "New Issue" or "New Merge Request" buttons + Given I disable issues and merge requests in project + When I visit project "Shop" page + Then I should not see "New Issue" button + And I should not see "New Merge Request" button + + Scenario: I should not see Project snippets + Given I disable snippets in project + When I visit project "Shop" page + Then I should not see "Snippets" button diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index cfb68bf1f5..0f71c32380 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -3,7 +3,7 @@ Feature: Project Shortcuts Background: Given I sign in as a user And I own a project - And I visit my project's home page + And I visit my project's commits page @javascript Scenario: Navigate to files tab @@ -12,6 +12,7 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to commits tab + Given I visit my project's files page Given I press "g" and "c" Then the active main tab should be Commits @@ -46,7 +47,11 @@ Feature: Project Shortcuts Then the active main tab should be Wiki @javascript - Scenario: Navigate to project feed - Given I visit my project's files page + Scenario: Navigate to project home Given I press "g" and "p" Then the active main tab should be Home + + @javascript + Scenario: Navigate to project feed + Given I press "g" and "e" + Then the active main tab should be Activity diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 90b966dd64..d3a77466a3 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -45,7 +45,7 @@ Feature: Project Source Browse Files Then I am redirected to the new file on new branch And I should see its new content - @javascript @tricky + @javascript Scenario: I can create file in empty repo Given I own an empty project And I visit my empty project page @@ -158,3 +158,10 @@ Feature: Project Source Browse Files Given I visit project source page for "6d394385cf567f80a8fd85055db1ab4c5295806f" And I click on ".gitignore" file in repo Then I don't see the permalink link + + @javascript + Scenario: I browse code with single quotes in the ref + Given I switch ref to 'test' + And I see the ref 'test' has been selected + And I visit the 'test' tree + Then I see the commit data diff --git a/features/project/source/multiselect_blob.feature b/features/project/source/multiselect_blob.feature deleted file mode 100644 index 63b7cb77a9..0000000000 --- a/features/project/source/multiselect_blob.feature +++ /dev/null @@ -1,85 +0,0 @@ -Feature: Project Source Multiselect Blob - Background: - Given I sign in as a user - And I own project "Shop" - And I visit ".gitignore" file in repo - - @javascript - Scenario: I click line 1 in file - When I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I shift-click line 1 in file - When I shift-click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I click line 1 then click line 2 in file - When I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I click line 2 in file - Then I should see "L2" as URI fragment - And I should see line 2 highlighted - - @javascript - Scenario: I click various line numbers to test multiselect - Then I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I shift-click line 2 in file - Then I should see "L1-2" as URI fragment - And I should see lines 1-2 highlighted - Then I shift-click line 3 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I click line 3 in file - Then I should see "L3" as URI fragment - And I should see line 3 highlighted - Then I shift-click line 1 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I shift-click line 5 in file - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted - Then I shift-click line 4 in file - Then I should see "L1-4" as URI fragment - And I should see lines 1-4 highlighted - Then I click line 5 in file - Then I should see "L5" as URI fragment - And I should see line 5 highlighted - Then I shift-click line 3 in file - Then I should see "L3-5" as URI fragment - And I should see lines 3-5 highlighted - Then I shift-click line 1 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I shift-click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I multiselect lines 1-5 and then go back and forward in history - When I click line 1 in file - And I shift-click line 3 in file - And I shift-click line 2 in file - And I shift-click line 5 in file - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted - Then I go back in history - Then I should see "L1-2" as URI fragment - And I should see lines 1-2 highlighted - Then I go back in history - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I go back in history - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I go forward in history - And I go forward in history - And I go forward in history - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted diff --git a/features/project/team_management.feature b/features/project/team_management.feature index 6cda225ea7..09a7df59df 100644 --- a/features/project/team_management.feature +++ b/features/project/team_management.feature @@ -3,13 +3,13 @@ Feature: Project Team Management Given I sign in as a user And I own project "Shop" And gitlab user "Mike" - And gitlab user "Sam" - And "Sam" is "Shop" developer + And gitlab user "Dmitriy" + And "Dmitriy" is "Shop" developer And I visit project "Shop" team page Scenario: See all team members Then I should be able to see myself in team - And I should see "Sam" in team list + And I should see "Dmitriy" in team list @javascript Scenario: Add user to project @@ -25,14 +25,14 @@ Feature: Project Team Management @javascript Scenario: Update user access - Given I should see "Sam" in team list as "Developer" - And I change "Sam" role to "Reporter" - And I should see "Sam" in team list as "Reporter" + Given I should see "Dmitriy" in team list as "Developer" + And I change "Dmitriy" role to "Reporter" + And I should see "Dmitriy" in team list as "Reporter" Scenario: Cancel team member - Given I click cancel link for "Sam" + Given I click cancel link for "Dmitriy" Then I visit project "Shop" team page - And I should not see "Sam" in team list + And I should not see "Dmitriy" in team list Scenario: Import team from another project Given I own project "Website" diff --git a/features/project/wiki.feature b/features/project/wiki.feature index 977cd609a1..2ebfa3c166 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -69,6 +69,11 @@ Feature: Project Wiki And I click on the "Pages" button Then I should see non-escaped link in the pages list + @javascript + Scenario: Creating an invalid new page + Given I create a New page with an invalid name + Then I should see an error message + @javascript Scenario: Edit Wiki page that has a path Given I create a New page with paths diff --git a/features/search.feature b/features/search.feature index def21e0092..1608e82467 100644 --- a/features/search.feature +++ b/features/search.feature @@ -44,3 +44,9 @@ Feature: Search Then I should see "Foo" link in the search results And I should not see "Bar" link in the search results + Scenario: I should see Wiki blobs + And project has Wiki content + When I click project "Shop" link + And I search for "Wiki content" + And I click "Wiki" link + Then I should see "test_wiki" link in the search results diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature index 6e8019c326..4f617b6bed 100644 --- a/features/snippets/snippets.feature +++ b/features/snippets/snippets.feature @@ -25,4 +25,15 @@ Feature: Snippets Scenario: I destroy "Personal snippet one" Given I visit snippet page "Personal snippet one" And I click link "Destroy" - Then I should not see "Personal snippet one" in snippets \ No newline at end of file + Then I should not see "Personal snippet one" in snippets + + Scenario: I create new internal snippet + Given I logout directly + And I sign in as an admin + Then I visit new snippet page + And I submit new internal snippet + Then I visit snippet page "Internal personal snippet one" + And I logout directly + Then I sign in as a user + Given I visit new snippet page + Then I visit snippet page "Internal personal snippet one" diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb new file mode 100644 index 0000000000..8f9ddb2899 --- /dev/null +++ b/features/steps/abuse_reports.rb @@ -0,0 +1,28 @@ +class Spinach::Features::AbuseReports < Spinach::FeatureSteps + include SharedAuthentication + + step 'I visit "Mike" user page' do + visit user_path(user_mike) + end + + step 'I click "Report abuse" button' do + click_link 'Report abuse' + end + + step 'I fill and submit abuse form' do + fill_in 'abuse_report_message', with: 'This user send spam' + click_button 'Send report' + end + + step 'I should see success message' do + page.should have_content 'Thank you for your report' + end + + step 'user "Mike" exists' do + user_mike + end + + def user_mike + @user_mike ||= create(:user, name: 'Mike') + end +end diff --git a/features/steps/admin/abuse_reports.rb b/features/steps/admin/abuse_reports.rb new file mode 100644 index 0000000000..0149416c91 --- /dev/null +++ b/features/steps/admin/abuse_reports.rb @@ -0,0 +1,15 @@ +class Spinach::Features::AdminAbuseReports < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedAdmin + + step 'I should see list of abuse reports' do + page.should have_content("Abuse Reports") + page.should have_content AbuseReport.first.message + page.should have_link("Remove user") + end + + step 'abuse reports exist' do + create(:abuse_report) + end +end diff --git a/features/steps/admin/applications.rb b/features/steps/admin/applications.rb index d59088fa3c..7c12cb9692 100644 --- a/features/steps/admin/applications.rb +++ b/features/steps/admin/applications.rb @@ -8,7 +8,7 @@ class Spinach::Features::AdminApplications < Spinach::FeatureSteps end step 'I should see application form' do - page.should have_content "New application" + expect(page).to have_content "New application" end step 'I fill application form out and submit' do @@ -18,9 +18,9 @@ class Spinach::Features::AdminApplications < Spinach::FeatureSteps end step 'I see application' do - page.should have_content "Application: test" - page.should have_content "Application Id" - page.should have_content "Secret" + expect(page).to have_content "Application: test" + expect(page).to have_content "Application Id" + expect(page).to have_content "Secret" end step 'I click edit' do @@ -28,28 +28,28 @@ class Spinach::Features::AdminApplications < Spinach::FeatureSteps end step 'I see edit application form' do - page.should have_content "Edit application" + expect(page).to have_content "Edit application" end step 'I change name of application and submit' do - page.should have_content "Edit application" + expect(page).to have_content "Edit application" fill_in :doorkeeper_application_name, with: 'test_changed' click_on "Submit" end step 'I see that application was changed' do - page.should have_content "test_changed" - page.should have_content "Application Id" - page.should have_content "Secret" + expect(page).to have_content "test_changed" + expect(page).to have_content "Application Id" + expect(page).to have_content "Secret" end step 'I click to remove application' do - within '.oauth-applications' do + page.within '.oauth-applications' do click_on "Destroy" end end step "I see that application is removed" do - page.find(".oauth-applications").should_not have_content "test_changed" + expect(page.find(".oauth-applications")).not_to have_content "test_changed" end end diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb index a35fa34a3a..f6daf85297 100644 --- a/features/steps/admin/broadcast_messages.rb +++ b/features/steps/admin/broadcast_messages.rb @@ -8,7 +8,7 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps end step 'I should be all broadcast messages' do - page.should have_content "Migration to new server" + expect(page).to have_content "Migration to new server" end step 'submit form with new broadcast message' do @@ -18,11 +18,11 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps end step 'I should be redirected to admin messages page' do - current_path.should == admin_broadcast_messages_path + expect(current_path).to eq admin_broadcast_messages_path end step 'I should see newly created broadcast message' do - page.should have_content 'Application update from 4:00 CST to 5:00 CST' + expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' end step 'submit form with new customized broadcast message' do @@ -35,7 +35,7 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps end step 'I should see a customized broadcast message' do - page.should have_content 'Application update from 4:00 CST to 5:00 CST' - page.should have_selector %(div[style="background-color:#f2dede;color:#b94a48"]) + expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' + expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"]) end end diff --git a/features/steps/admin/deploy_keys.rb b/features/steps/admin/deploy_keys.rb index fb0b611762..56787eeb6b 100644 --- a/features/steps/admin/deploy_keys.rb +++ b/features/steps/admin/deploy_keys.rb @@ -10,21 +10,10 @@ class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps step 'I should see all public deploy keys' do DeployKey.are_public.each do |p| - page.should have_content p.title + expect(page).to have_content p.title end end - step 'I click on first deploy key' do - click_link DeployKey.are_public.first.title - end - - step 'I should see deploy key details' do - deploy_key = DeployKey.are_public.first - current_path.should == admin_deploy_key_path(deploy_key) - page.should have_content(deploy_key.title) - page.should have_content(deploy_key.key) - end - step 'I visit admin deploy key page' do visit admin_deploy_key_path(deploy_key) end @@ -44,11 +33,11 @@ class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps end step 'I should be on admin deploy keys page' do - current_path.should == admin_deploy_keys_path + expect(current_path).to eq admin_deploy_keys_path end step 'I should see newly created deploy key' do - page.should have_content(deploy_key.title) + expect(page).to have_content(deploy_key.title) end def deploy_key diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 721460b937..d27634858a 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -28,48 +28,63 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end step 'I should see newly created group' do - page.should have_content "Group: gitlab" - page.should have_content "Group description" + expect(page).to have_content "Group: gitlab" + expect(page).to have_content "Group description" end step 'I should be redirected to group page' do - current_path.should == admin_group_path(Group.find_by(path: 'gitlab')) + expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab')) end When 'I select user "John Doe" from user list as "Reporter"' do select2(user_john.id, from: "#user_ids", multiple: true) - within "#new_project_member" do + page.within "#new_project_member" do + select "Reporter", from: "access_level" + end + click_button "Add users to group" + end + + When 'I select user "johndoe@gitlab.com" from user list as "Reporter"' do + select2('johndoe@gitlab.com', from: "#user_ids", multiple: true) + page.within "#new_project_member" do select "Reporter", from: "access_level" end click_button "Add users to group" end step 'I should see "John Doe" in team list in every project as "Reporter"' do - within ".group-users-list" do - page.should have_content "John Doe" - page.should have_content "Reporter" + page.within ".group-users-list" do + expect(page).to have_content "John Doe" + expect(page).to have_content "Reporter" + end + end + + step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do + page.within ".group-users-list" do + expect(page).to have_content "johndoe@gitlab.com (invited)" + expect(page).to have_content "Reporter" end end step 'I should be all groups' do Group.all.each do |group| - page.should have_content group.name + expect(page).to have_content group.name end end step 'we have user "John Doe" in group' do - current_group.add_user(user_john, Gitlab::Access::REPORTER) + current_group.add_reporter(user_john) end step 'I remove user "John Doe" from group' do - within "#user_#{user_john.id}" do + page.within "#user_#{user_john.id}" do click_link 'Remove user from group' end end step 'I should not see "John Doe" in team list' do - within ".group-users-list" do - page.should_not have_content "John Doe" + page.within ".group-users-list" do + expect(page).not_to have_content "John Doe" end end diff --git a/features/steps/admin/logs.rb b/features/steps/admin/logs.rb index 904e546865..f9e49588c7 100644 --- a/features/steps/admin/logs.rb +++ b/features/steps/admin/logs.rb @@ -4,8 +4,8 @@ class Spinach::Features::AdminLogs < Spinach::FeatureSteps include SharedAdmin step 'I should see tabs with available logs' do - page.should have_content 'production.log' - page.should have_content 'githost.log' - page.should have_content 'application.log' + expect(page).to have_content 'production.log' + expect(page).to have_content 'githost.log' + expect(page).to have_content 'application.log' end end diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index 9be4d39d2d..655f189527 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -5,7 +5,7 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I should see all projects' do Project.all.each do |p| - page.should have_content p.name_with_namespace + expect(page).to have_content p.name_with_namespace end end @@ -15,9 +15,9 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I should see project details' do project = Project.first - current_path.should == admin_namespace_project_path(project.namespace, project) - page.should have_content(project.name_with_namespace) - page.should have_content(project.creator.name) + expect(current_path).to eq admin_namespace_project_path(project.namespace, project) + expect(page).to have_content(project.name_with_namespace) + expect(page).to have_content(project.creator.name) end step 'I visit admin project page' do @@ -34,8 +34,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end step 'I should see project transfered' do - page.should have_content 'Web / ' + project.name - page.should have_content 'Namespace: Web' + expect(page).to have_content 'Web / ' + project.name + expect(page).to have_content 'Namespace: Web' end def project diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb index 87d4e969ff..7a6aec23af 100644 --- a/features/steps/admin/settings.rb +++ b/features/steps/admin/settings.rb @@ -6,14 +6,14 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps step 'I modify settings and save form' do uncheck 'Gravatar enabled' - fill_in 'Home page url', with: 'https://about.gitlab.com/' + fill_in 'Home page URL', with: 'https://about.gitlab.com/' click_button 'Save' end step 'I should see application settings saved' do - current_application_settings.gravatar_enabled.should be_false - current_application_settings.home_page_url.should == 'https://about.gitlab.com/' - page.should have_content 'Application settings saved successfully' + expect(current_application_settings.gravatar_enabled).to be_falsey + expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" + expect(page).to have_content "Application settings saved successfully" end step 'I click on "Service Templates"' do @@ -31,17 +31,28 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps page.check('Comments') page.check('Issues events') page.check('Merge Request events') - fill_in 'Webhook', with: "http://localhost" click_on 'Save' end + step 'I fill out Slack settings' do + fill_in 'Webhook', with: 'http://localhost' + fill_in 'Username', with: 'test_user' + fill_in 'Channel', with: '#test_channel' + end + step 'I should see service template settings saved' do - page.should have_content 'Application settings saved successfully' + expect(page).to have_content 'Application settings saved successfully' end step 'I should see all checkboxes checked' do - all('input[type=checkbox]').each do |checkbox| - checkbox.should be_checked + page.all('input[type=checkbox]').each do |checkbox| + expect(checkbox).to be_checked end end + + step 'I should see Slack settings saved' do + expect(find_field('Webhook').value).to eq 'http://localhost' + expect(find_field('Username').value).to eq 'test_user' + expect(find_field('Channel').value).to eq '#test_channel' + end end diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb index e138309724..2e17d5c4c2 100644 --- a/features/steps/admin/users.rb +++ b/features/steps/admin/users.rb @@ -3,9 +3,17 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps include SharedPaths include SharedAdmin + before do + allow(Devise).to receive(:omniauth_providers).and_return([:twitter, :twitter_updated]) + end + + after do + allow(Devise).to receive(:omniauth_providers).and_call_original + end + step 'I should see all users' do User.all.each do |user| - page.should have_content user.name + expect(page).to have_content user.name end end @@ -23,13 +31,13 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'See username error message' do - within "#error_explanation" do - page.should have_content "Username" + page.within "#error_explanation" do + expect(page).to have_content "Username" end end step 'Not changed form action url' do - page.should have_selector %(form[action="/admin/users/#{@user.username}"]) + expect(page).to have_selector %(form[action="/admin/users/#{@user.username}"]) end step 'I submit modified user' do @@ -38,7 +46,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I see user attributes changed' do - page.should have_content 'Can create groups: Yes' + expect(page).to have_content 'Can create groups: Yes' end step 'click edit on my user' do @@ -53,7 +61,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I see the secondary email' do - page.should have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}" + expect(page).to have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}" end step 'I click remove secondary email' do @@ -61,7 +69,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I should not see secondary email anymore' do - page.should_not have_content "Secondary email:" + expect(page).not_to have_content "Secondary email:" end step 'user "Mike" with groups and projects' do @@ -71,7 +79,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps project.team << [user, :developer] group = create(:group) - group.add_user(user, Gitlab::Access::DEVELOPER) + group.add_developer(user) end step 'click on "Mike" link' do @@ -79,8 +87,8 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I should see user "Mike" details' do - page.should have_content 'Account' - page.should have_content 'Personal projects limit' + expect(page).to have_content 'Account' + expect(page).to have_content 'Personal projects limit' end step 'user "Pete" with ssh keys' do @@ -94,8 +102,8 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I should see key list' do - page.should have_content 'ssh-rsa Key2' - page.should have_content 'ssh-rsa Key1' + expect(page).to have_content 'ssh-rsa Key2' + expect(page).to have_content 'ssh-rsa Key1' end step 'I click on the key title' do @@ -103,8 +111,8 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I should see key details' do - page.should have_content 'ssh-rsa Key2' - page.should have_content 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2' + expect(page).to have_content 'ssh-rsa Key2' + expect(page).to have_content 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2' end step 'I click on remove key' do @@ -112,6 +120,46 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I should see the key removed' do - page.should_not have_content 'ssh-rsa Key2' + expect(page).not_to have_content 'ssh-rsa Key2' + end + + step 'user "Pete" with twitter account' do + @user = create(:user, name: 'Pete') + @user.identities.create!(extern_uid: '123456', provider: 'twitter') + end + + step 'I visit "Pete" identities page in admin' do + visit admin_user_identities_path(@user) + end + + step 'I should see twitter details' do + expect(page).to have_content 'Pete' + expect(page).to have_content 'twitter' + end + + step 'I modify twitter identity' do + find('.table').find(:link, 'Edit').click + fill_in 'identity_extern_uid', with: '654321' + select 'twitter_updated', from: 'identity_provider' + click_button 'Save changes' + end + + step 'I should see twitter details updated' do + expect(page).to have_content 'Pete' + expect(page).to have_content 'twitter_updated' + expect(page).to have_content '654321' + end + + step 'I remove twitter identity' do + click_link 'Delete' + end + + step 'I should not see twitter details' do + expect(page).to have_content 'Pete' + expect(page).to_not have_content 'twitter' + end + + step 'click on ssh keys tab' do + click_link 'SSH keys' end end diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb index 969baf9228..36e092f50c 100644 --- a/features/steps/dashboard/archived_projects.rb +++ b/features/steps/dashboard/archived_projects.rb @@ -9,14 +9,14 @@ class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps end step 'I should see "Shop" project link' do - page.should have_link "Shop" + expect(page).to have_link "Shop" end step 'I should not see "Forum" project link' do - page.should_not have_link "Forum" + expect(page).not_to have_link "Forum" end step 'I should see "Forum" project link' do - page.should have_link "Forum" + expect(page).to have_link "Forum" end end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 8508b2a809..cb3a80cac2 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -4,16 +4,16 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps include SharedProject step 'I should see "New Project" link' do - page.should have_link "New project" + expect(page).to have_link "New project" end step 'I should see "Shop" project link' do - page.should have_link "Shop" + expect(page).to have_link "Shop" end step 'I should see last push widget' do - page.should have_content "You pushed to fix" - page.should have_link "Create Merge Request" + expect(page).to have_content "You pushed to fix" + expect(page).to have_link "Create Merge Request" end step 'I click "Create Merge Request" link' do @@ -21,14 +21,14 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I see prefilled new Merge Request page' do - current_path.should == new_namespace_project_merge_request_path(@project.namespace, @project) - find("#merge_request_target_project_id").value.should == @project.id.to_s - find("#merge_request_source_branch").value.should == "fix" - find("#merge_request_target_branch").value.should == "master" + expect(current_path).to eq new_namespace_project_merge_request_path(@project.namespace, @project) + expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s + expect(find("input#merge_request_source_branch").value).to eq "fix" + expect(find("input#merge_request_target_branch").value).to eq "master" end step 'user with name "John Doe" joined project "Shop"' do - user = create(:user, {name: "John Doe"}) + user = create(:user, { name: "John Doe" }) project.team << [user, :master] Event.create( project: project, @@ -38,7 +38,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I should see "John Doe joined project Shop" event' do - page.should have_content "John Doe joined project #{project.name_with_namespace}" + expect(page).to have_content "John Doe joined project #{project.name_with_namespace}" end step 'user with name "John Doe" left project "Shop"' do @@ -51,7 +51,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I should see "John Doe left project Shop" event' do - page.should have_content "John Doe left project #{project.name_with_namespace}" + expect(page).to have_content "John Doe left project #{project.name_with_namespace}" end step 'I have group with projects' do @@ -64,13 +64,13 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I should see projects list' do @user.authorized_projects.all.each do |project| - page.should have_link project.name_with_namespace + expect(page).to have_link project.name_with_namespace end end step 'I should see groups list' do Group.all.each do |group| - page.should have_link group.name + expect(page).to have_link group.name end end @@ -80,6 +80,6 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I should see 1 project at group list' do - find('span.last_activity/span').should have_content('1') + expect(find('span.last_activity/span')).to have_content('1') end end diff --git a/features/steps/dashboard/event_filters.rb b/features/steps/dashboard/event_filters.rb index 3da3d62d0c..726b37cfde 100644 --- a/features/steps/dashboard/event_filters.rb +++ b/features/steps/dashboard/event_filters.rb @@ -4,27 +4,27 @@ class Spinach::Features::EventFilters < Spinach::FeatureSteps include SharedProject step 'I should see push event' do - page.should have_selector('span.pushed') + expect(page).to have_selector('span.pushed') end step 'I should not see push event' do - page.should_not have_selector('span.pushed') + expect(page).not_to have_selector('span.pushed') end step 'I should see new member event' do - page.should have_selector('span.joined') + expect(page).to have_selector('span.joined') end step 'I should not see new member event' do - page.should_not have_selector('span.joined') + expect(page).not_to have_selector('span.joined') end step 'I should see merge request event' do - page.should have_selector('span.accepted') + expect(page).to have_selector('span.accepted') end step 'I should not see merge request event' do - page.should_not have_selector('span.accepted') + expect(page).not_to have_selector('span.accepted') end step 'this project has push event' do @@ -52,7 +52,7 @@ class Spinach::Features::EventFilters < Spinach::FeatureSteps end step 'this project has new member event' do - user = create(:user, {name: "John Doe"}) + user = create(:user, { name: "John Doe" }) Event.create( project: @project, author_id: user.id, diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb index 8384df2fb5..0c6a0ae372 100644 --- a/features/steps/dashboard/group.rb +++ b/features/steps/dashboard/group.rb @@ -17,29 +17,29 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps end step 'I should not see the "Leave" button for group "Owned"' do - find(:css, 'li', text: "Owner").should_not have_selector(:css, 'i.fa.fa-sign-out') + expect(find(:css, 'li', text: "Owner")).not_to have_selector(:css, 'i.fa.fa-sign-out') # poltergeist always confirms popups. end step 'I should not see the "Leave" button for groupr "Guest"' do - find(:css, 'li', text: "Guest").should_not have_selector(:css, 'i.fa.fa-sign-out') + expect(find(:css, 'li', text: "Guest")).not_to have_selector(:css, 'i.fa.fa-sign-out') # poltergeist always confirms popups. end step 'I should see group "Owned" in group list' do - page.should have_content("Owned") + expect(page).to have_content("Owned") end step 'I should not see group "Owned" in group list' do - page.should_not have_content("Owned") + expect(page).not_to have_content("Owned") end step 'I should see group "Guest" in group list' do - page.should have_content("Guest") + expect(page).to have_content("Guest") end step 'I should not see group "Guest" in group list' do - page.should_not have_content("Guest") + expect(page).not_to have_content("Guest") end step 'I click new group link' do @@ -53,11 +53,15 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps end step 'I should be redirected to group "Samurai" page' do - current_path.should == group_path(Group.find_by(name: 'Samurai')) + expect(current_path).to eq group_path(Group.find_by(name: 'Samurai')) end step 'I should see newly created group "Samurai"' do - page.should have_content "Samurai" - page.should have_content "Tokugawa Shogunate" + expect(page).to have_content "Samurai" + expect(page).to have_content "Tokugawa Shogunate" + end + + step 'I should see the "Can not leave message"' do + expect(page).to have_content "You can not leave Owned group because you're the last owner" end end diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb index ef433c57c6..86ab31a58a 100644 --- a/features/steps/dashboard/help.rb +++ b/features/steps/dashboard/help.rb @@ -12,7 +12,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps end step 'I should see "Rake Tasks" page markdown rendered' do - page.should have_content "Gather information about GitLab and the system it runs on" + expect(page).to have_content "Gather information about GitLab and the system it runs on" end step 'Header "Rebuild project satellites" should have correct ids and links' do diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index 60da36e86d..cbe54e2dc7 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -46,11 +46,11 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps end def should_see(issue) - page.should have_content(issue.title[0..10]) + expect(page).to have_content(issue.title[0..10]) end def should_not_see(issue) - page.should_not have_content(issue.title[0..10]) + expect(page).not_to have_content(issue.title[0..10]) end def assigned_issue diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index 9d92082bb8..cec8d06ade 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -50,11 +50,11 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps end def should_see(merge_request) - page.should have_content(merge_request.title[0..10]) + expect(page).to have_content(merge_request.title[0..10]) end def should_not_see(merge_request) - page.should_not have_content(merge_request.title[0..10]) + expect(page).not_to have_content(merge_request.title[0..10]) end def assigned_merge_request diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 5e588ceb78..d4440c1fb4 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -4,11 +4,13 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps include SharedProject step 'I click "New project" link' do - click_link "New project" + page.within('.content') do + click_link "New project" + end end step 'I see "New project" page' do - page.should have_content("Project path") + expect(page).to have_content('Project path') end step 'I click on "Import project from GitHub"' do @@ -17,11 +19,11 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps step 'I see instructions on how to import from GitHub' do github_modal = first('.modal-body') - github_modal.should be_visible - github_modal.should have_content "To enable importing projects from GitHub" + expect(github_modal).to be_visible + expect(github_modal).to have_content "To enable importing projects from GitHub" - all('.modal-body').each do |element| - element.should_not be_visible unless element == github_modal + page.all('.modal-body').each do |element| + expect(element).not_to be_visible unless element == github_modal end end end diff --git a/features/steps/dashboard/starred_projects.rb b/features/steps/dashboard/starred_projects.rb index b9ad2f13e2..59c73fe63f 100644 --- a/features/steps/dashboard/starred_projects.rb +++ b/features/steps/dashboard/starred_projects.rb @@ -8,8 +8,8 @@ class Spinach::Features::DashboardStarredProjects < Spinach::FeatureSteps end step 'I should not see project "Shop"' do - within 'aside' do - page.should_not have_content('Shop') + page.within 'aside' do + expect(page).not_to have_content('Shop') end end end diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index 0c2127d4c4..87cd33c37e 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -19,7 +19,7 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps step '"John Doe" is owner of group "TestGroup"' do group = Group.find_by(name: "TestGroup") || create(:group, name: "TestGroup") user = create(:user, name: "John Doe") - group.add_user(user, Gitlab::Access::OWNER) + group.add_owner(user) end step 'I visit group "TestGroup" page' do @@ -39,19 +39,19 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps end step 'I should not see project "Enterprise" items' do - page.should_not have_content "Enterprise" + expect(page).not_to have_content "Enterprise" end step 'I should see project "Internal" items' do - page.should have_content "Internal" + expect(page).to have_content "Internal" end step 'I should not see project "Internal" items' do - page.should_not have_content "Internal" + expect(page).not_to have_content "Internal" end step 'I should see project "Community" items' do - page.should have_content "Community" + expect(page).to have_content "Community" end step 'I change filter to Everyone\'s' do @@ -59,11 +59,11 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps end step 'I should see group member "John Doe"' do - page.should have_content "John Doe" + expect(page).to have_content "John Doe" end step 'I should not see member roles' do - body.should_not match(%r{owner|developer|reporter|guest}i) + expect(body).not_to match(%r{owner|developer|reporter|guest}i) end protected diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index 26b71406bd..8b498e7b4a 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -4,56 +4,56 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps include SharedProject step 'I should see project "Empty Public Project"' do - page.should have_content "Empty Public Project" + expect(page).to have_content "Empty Public Project" end step 'I should see public project details' do - page.should have_content '32 branches' - page.should have_content '16 tags' + expect(page).to have_content '32 branches' + expect(page).to have_content '16 tags' end step 'I should see project readme' do - page.should have_content 'README.md' + expect(page).to have_content 'README.md' end step 'I should see empty public project details' do - page.should have_content 'Git global setup' + expect(page).to have_content 'Git global setup' end step 'I should see empty public project details with http clone info' do project = Project.find_by(name: 'Empty Public Project') - all(:css, '.git-empty .clone').each do |element| - element.text.should include(project.http_url_to_repo) + page.all(:css, '.git-empty .clone').each do |element| + expect(element.text).to include(project.http_url_to_repo) end end step 'I should see empty public project details with ssh clone info' do project = Project.find_by(name: 'Empty Public Project') - all(:css, '.git-empty .clone').each do |element| - element.text.should include(project.url_to_repo) + page.all(:css, '.git-empty .clone').each do |element| + expect(element.text).to include(project.url_to_repo) end end step 'I should see project "Community" home page' do - within '.navbar-gitlab .title' do - page.should have_content 'Community' + page.within '.navbar-gitlab .title' do + expect(page).to have_content 'Community' end end step 'I should see project "Internal" home page' do - within '.navbar-gitlab .title' do - page.should have_content 'Internal' + page.within '.navbar-gitlab .title' do + expect(page).to have_content 'Internal' end end step 'I should see an http link to the repository' do project = Project.find_by(name: 'Community') - page.should have_field('project_clone', with: project.http_url_to_repo) + expect(page).to have_field('project_clone', with: project.http_url_to_repo) end step 'I should see an ssh link to the repository' do project = Project.find_by(name: 'Community') - page.should have_field('project_clone', with: project.url_to_repo) + expect(page).to have_field('project_clone', with: project.url_to_repo) end step 'I visit "Community" issues page' do @@ -70,9 +70,9 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps step 'I should see list of issues for "Community" project' do - page.should have_content "Bug" - page.should have_content public_project.name - page.should have_content "New feature" + expect(page).to have_content "Bug" + expect(page).to have_content public_project.name + expect(page).to have_content "New feature" end step 'I visit "Internal" issues page' do @@ -89,9 +89,9 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps step 'I should see list of issues for "Internal" project' do - page.should have_content "Internal Bug" - page.should have_content internal_project.name - page.should have_content "New internal feature" + expect(page).to have_content "Internal Bug" + expect(page).to have_content internal_project.name + expect(page).to have_content "New internal feature" end step 'I visit "Community" merge requests page' do @@ -107,8 +107,8 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps end step 'I should see list of merge requests for "Community" project' do - page.should have_content public_project.name - page.should have_content public_merge_request.source_project.name + expect(page).to have_content public_project.name + expect(page).to have_content public_merge_request.source_project.name end step 'I visit "Internal" merge requests page' do @@ -124,8 +124,8 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps end step 'I should see list of merge requests for "Internal" project' do - page.should have_content internal_project.name - page.should have_content internal_merge_request.source_project.name + expect(page).to have_content internal_project.name + expect(page).to have_content internal_merge_request.source_project.name end def internal_project @@ -145,4 +145,3 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps @public_merge_request ||= MergeRequest.find_by!(title: 'Bug fix for public project') end end - diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 228b83e5fd..46e1f4d099 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -5,6 +5,10 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedUser include Select2Helper + step 'I should see back to dashboard button' do + expect(page).to have_content 'Back to Dashboard' + end + step 'gitlab user "Mike"' do create(:user, name: "Mike") end @@ -16,7 +20,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I select "Mike" as "Reporter"' do user = User.find_by(name: "Mike") - within ".users-group-form" do + page.within ".users-group-form" do select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end @@ -25,14 +29,14 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see "Mike" in team list as "Reporter"' do - within '.well-list' do - page.should have_content('Mike') - page.should have_content('Reporter') + page.within '.well-list' do + expect(page).to have_content('Mike') + expect(page).to have_content('Reporter') end end step 'I select "sjobs@apple.com" as "Reporter"' do - within ".users-group-form" do + page.within ".users-group-form" do select2("sjobs@apple.com", from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end @@ -41,39 +45,39 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do - within '.well-list' do - page.should have_content('sjobs@apple.com') - page.should have_content('invited') - page.should have_content('Reporter') + page.within '.well-list' do + expect(page).to have_content('sjobs@apple.com') + expect(page).to have_content('invited') + expect(page).to have_content('Reporter') end end step 'I should see group "Owned" projects list' do Group.find_by(name: "Owned").projects.each do |project| - page.should have_link project.name + expect(page).to have_link project.name end end step 'I should see projects activity feed' do - page.should have_content 'closed issue' + expect(page).to have_content 'closed issue' end step 'I should see issues from group "Owned" assigned to me' do assigned_to_me(:issues).each do |issue| - page.should have_content issue.title + expect(page).to have_content issue.title end end step 'I should see merge requests from group "Owned" assigned to me' do assigned_to_me(:merge_requests).each do |issue| - page.should have_content issue.title[0..80] + expect(page).to have_content issue.title[0..80] end end step 'I select user "Mary Jane" from list with role "Reporter"' do user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane") click_button 'Add members' - within ".users-group-form" do + page.within ".users-group-form" do select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end @@ -82,22 +86,22 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I should see user "John Doe" in team list' do projects_with_access = find(".panel .well-list") - projects_with_access.should have_content("John Doe") + expect(projects_with_access).to have_content("John Doe") end step 'I should not see user "John Doe" in team list' do projects_with_access = find(".panel .well-list") - projects_with_access.should_not have_content("John Doe") + expect(projects_with_access).not_to have_content("John Doe") end step 'I should see user "Mary Jane" in team list' do projects_with_access = find(".panel .well-list") - projects_with_access.should have_content("Mary Jane") + expect(projects_with_access).to have_content("Mary Jane") end step 'I should not see user "Mary Jane" in team list' do projects_with_access = find(".panel .well-list") - projects_with_access.should_not have_content("Mary Jane") + expect(projects_with_access).not_to have_content("Mary Jane") end step 'project from group "Owned" has issues assigned to me' do @@ -122,28 +126,28 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see new group "Owned" name' do - within ".navbar-gitlab" do - page.should have_content "new-name" + page.within ".navbar-gitlab" do + expect(page).to have_content "new-name" end end step 'I change group "Owned" avatar' do - attach_file(:group_avatar, File.join(Rails.root, 'public', 'gitlab_logo.png')) + attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')) click_button "Save group" Group.find_by(name: "Owned").reload end step 'I should see new group "Owned" avatar' do - Group.find_by(name: "Owned").avatar.should be_instance_of AvatarUploader - Group.find_by(name: "Owned").avatar.url.should == "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/gitlab_logo.png" + expect(Group.find_by(name: "Owned").avatar).to be_instance_of AvatarUploader + expect(Group.find_by(name: "Owned").avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/banana_sample.gif" end step 'I should see the "Remove avatar" button' do - page.should have_link("Remove avatar") + expect(page).to have_link("Remove avatar") end step 'I have group "Owned" avatar' do - attach_file(:group_avatar, File.join(Rails.root, 'public', 'gitlab_logo.png')) + attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')) click_button "Save group" Group.find_by(name: "Owned").reload end @@ -154,11 +158,11 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should not see group "Owned" avatar' do - Group.find_by(name: "Owned").avatar?.should be_false + expect(Group.find_by(name: "Owned").avatar?).to eq false end step 'I should not see the "Remove avatar" button' do - page.should_not have_link("Remove avatar") + expect(page).not_to have_link("Remove avatar") end step 'I click on the "Remove User From Group" button for "John Doe"' do @@ -172,17 +176,17 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should not see the "Remove User From Group" button for "John Doe"' do - find(:css, 'li', text: "John Doe").should_not have_selector(:css, 'a.btn-remove') + expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') # poltergeist always confirms popups. end step 'I should not see the "Remove User From Group" button for "Mary Jane"' do - find(:css, 'li', text: "Mary Jane").should_not have_selector(:css, 'a.btn-remove') + expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove') # poltergeist always confirms popups. end step 'I search for \'Mary\' member' do - within '.member-search-form' do + page.within '.member-search-form' do fill_in 'search', with: 'Mary' click_button 'Search' end @@ -193,7 +197,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see group milestones index page has no milestones' do - page.should have_content('No milestones to show') + expect(page).to have_content('No milestones to show') end step 'Group has projects with milestones' do @@ -201,10 +205,10 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see group milestones index page with milestones' do - page.should have_content('Version 7.2') - page.should have_content('GL-113') - page.should have_link('2 Issues', href: group_milestone_path("owned", "version-7-2", title: "Version 7.2")) - page.should have_link('3 Merge Requests', href: group_milestone_path("owned", "gl-113", title: "GL-113")) + expect(page).to have_content('Version 7.2') + expect(page).to have_content('GL-113') + expect(page).to have_link('2 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2")) + expect(page).to have_link('3 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113")) end step 'I click on one group milestone' do @@ -212,14 +216,14 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see group milestone with descriptions and expiry date' do - page.should have_content('expires at Aug 20, 2114') + expect(page).to have_content('expires at Aug 20, 2114') end step 'I should see group milestone with all issues and MRs assigned to that milestone' do - page.should have_content('Milestone GL-113') - page.should have_content('Progress: 0 closed – 4 open') - page.should have_link(@issue1.title, href: namespace_project_issue_path(@project1.namespace, @project1, @issue1)) - page.should have_link(@mr3.title, href: namespace_project_merge_request_path(@project3.namespace, @project3, @mr3)) + expect(page).to have_content('Milestone GL-113') + expect(page).to have_content('Progress: 0 closed – 4 open') + expect(page).to have_link(@issue1.title, href: namespace_project_issue_path(@project1.namespace, @project1, @issue1)) + expect(page).to have_link(@mr3.title, href: namespace_project_merge_request_path(@project3.namespace, @project3, @mr3)) end protected diff --git a/features/steps/invites.rb b/features/steps/invites.rb index d051cc3edc..5e8feff509 100644 --- a/features/steps/invites.rb +++ b/features/steps/invites.rb @@ -6,7 +6,7 @@ class Spinach::Features::Invites < Spinach::FeatureSteps step '"John Doe" has invited "user@example.com" to group "Owned"' do user = User.find_by(name: "John Doe") group = Group.find_by(name: "Owned") - group.add_user("user@example.com", Gitlab::Access::DEVELOPER, user) + group.add_developer("user@example.com", user) end step 'I visit the invitation page' do diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb index 8595ee876a..4724a32627 100644 --- a/features/steps/profile/active_tab.rb +++ b/features/steps/profile/active_tab.rb @@ -15,11 +15,11 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps ensure_active_main_tab('SSH Keys') end - step 'the active main tab should be Design' do - ensure_active_main_tab('Design') + step 'the active main tab should be Preferences' do + ensure_active_main_tab('Preferences') end - step 'the active main tab should be History' do - ensure_active_main_tab('History') + step 'the active main tab should be Audit Log' do + ensure_active_main_tab('Audit Log') end end diff --git a/features/steps/profile/emails.rb b/features/steps/profile/emails.rb index 2b6ac37d86..10ebe70536 100644 --- a/features/steps/profile/emails.rb +++ b/features/steps/profile/emails.rb @@ -6,9 +6,9 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps end step 'I should see my emails' do - page.should have_content(@user.email) + expect(page).to have_content(@user.email) @user.emails.each do |email| - page.should have_content(email.email) + expect(page).to have_content(email.email) end end @@ -19,14 +19,14 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps step 'I should see new email "my@email.com"' do email = @user.emails.find_by(email: "my@email.com") - email.should_not be_nil - page.should have_content("my@email.com") + expect(email).not_to be_nil + expect(page).to have_content("my@email.com") end step 'I should not see email "my@email.com"' do email = @user.emails.find_by(email: "my@email.com") - email.should be_nil - page.should_not have_content("my@email.com") + expect(email).to be_nil + expect(page).not_to have_content("my@email.com") end step 'I click link "Remove" for "my@email.com"' do @@ -43,6 +43,6 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps step 'I should not have @user.email added' do email = @user.emails.find_by(email: @user.email) - email.should be_nil + expect(email).to be_nil end end diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index 13e93618eb..447ea6d9d1 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -7,6 +7,6 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps end step 'I should see global notifications settings' do - page.should have_content "Notifications Settings" + expect(page).to have_content "Notifications" end end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 791982d16c..8cf24705a5 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -3,44 +3,46 @@ class Spinach::Features::Profile < Spinach::FeatureSteps include SharedPaths step 'I should see my profile info' do - page.should have_content "Profile Settings" + expect(page).to have_content "This information will appear on your profile" end step 'I change my profile info' do - fill_in "user_skype", with: "testskype" - fill_in "user_linkedin", with: "testlinkedin" - fill_in "user_twitter", with: "testtwitter" - fill_in "user_website_url", with: "testurl" - fill_in "user_location", with: "Ukraine" - click_button "Save changes" + fill_in 'user_skype', with: 'testskype' + fill_in 'user_linkedin', with: 'testlinkedin' + fill_in 'user_twitter', with: 'testtwitter' + fill_in 'user_website_url', with: 'testurl' + fill_in 'user_location', with: 'Ukraine' + fill_in 'user_bio', with: 'I <3 GitLab' + click_button 'Save changes' @user.reload end step 'I should see new profile info' do - @user.skype.should == 'testskype' - @user.linkedin.should == 'testlinkedin' - @user.twitter.should == 'testtwitter' - @user.website_url.should == 'testurl' - find("#user_location").value.should == "Ukraine" + expect(@user.skype).to eq 'testskype' + expect(@user.linkedin).to eq 'testlinkedin' + expect(@user.twitter).to eq 'testtwitter' + expect(@user.website_url).to eq 'testurl' + expect(@user.bio).to eq 'I <3 GitLab' + expect(find('#user_location').value).to eq 'Ukraine' end step 'I change my avatar' do - attach_file(:user_avatar, File.join(Rails.root, 'public', 'gitlab_logo.png')) + attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')) click_button "Save changes" @user.reload end step 'I should see new avatar' do - @user.avatar.should be_instance_of AvatarUploader - @user.avatar.url.should == "/uploads/user/avatar/#{ @user.id }/gitlab_logo.png" + expect(@user.avatar).to be_instance_of AvatarUploader + expect(@user.avatar.url).to eq "/uploads/user/avatar/#{ @user.id }/banana_sample.gif" end step 'I should see the "Remove avatar" button' do - page.should have_link("Remove avatar") + expect(page).to have_link("Remove avatar") end step 'I have an avatar' do - attach_file(:user_avatar, File.join(Rails.root, 'public', 'gitlab_logo.png')) + attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')) click_button "Save changes" @user.reload end @@ -51,15 +53,19 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see my gravatar' do - @user.avatar?.should be_false + expect(@user.avatar?).to eq false end step 'I should not see the "Remove avatar" button' do - page.should_not have_link("Remove avatar") + expect(page).not_to have_link("Remove avatar") + end + + step 'I should see the gravatar host link' do + expect(page).to have_link("gravatar.com") end step 'I try change my password w/o old one' do - within '.update-password' do + page.within '.update-password' do fill_in "user_password", with: "22233344" fill_in "user_password_confirmation", with: "22233344" click_button "Save" @@ -67,7 +73,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I change my password' do - within '.update-password' do + page.within '.update-password' do fill_in "user_current_password", with: "12345678" fill_in "user_password", with: "22233344" fill_in "user_password_confirmation", with: "22233344" @@ -76,7 +82,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I unsuccessfully change my password' do - within '.update-password' do + page.within '.update-password' do fill_in "user_current_password", with: "12345678" fill_in "user_password", with: "password" fill_in "user_password_confirmation", with: "confirmation" @@ -85,23 +91,27 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step "I should see a missing password error message" do - page.should have_content "You must provide a valid current password" + page.within ".flash-container" do + expect(page).to have_content "You must provide a valid current password" + end end step "I should see a password error message" do - page.should have_content "Password confirmation doesn't match" + page.within '.alert' do + expect(page).to have_content "Password confirmation doesn't match" + end end step 'I reset my token' do - within '.update-token' do + page.within '.update-token' do @old_token = @user.private_token click_button "Reset" end end step 'I should see new token' do - find("#token").value.should_not == @old_token - find("#token").value.should == @user.reload.private_token + expect(find("#token").value).not_to eq @old_token + expect(find("#token").value).to eq @user.reload.private_token end step 'I have activity' do @@ -109,28 +119,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see my activity' do - page.should have_content "#{current_user.name} closed issue" - end - - step "I change my application theme" do - within '.application-theme' do - choose "Violet" - end - end - - step "I change my code preview theme" do - within '.code-preview-theme' do - choose "Solarized dark" - end - end - - step "I should see the theme change immediately" do - page.should have_selector('body.ui_color') - page.should_not have_selector('body.ui_basic') - end - - step "I should receive feedback that the changes were saved" do - page.should have_content("saved") + expect(page).to have_content "Signed in with standard authentication" end step 'my password is expired' do @@ -139,11 +128,11 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step "I am not an ldap user" do current_user.identities.delete - current_user.ldap_user?.should be_false + expect(current_user.ldap_user?).to eq false end step 'I redirected to expired password page' do - current_path.should == new_profile_password_path + expect(current_path).to eq new_profile_password_path end step 'I submit new password' do @@ -154,26 +143,26 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I redirected to sign in page' do - current_path.should == new_user_session_path + expect(current_path).to eq new_user_session_path end step 'I should be redirected to password page' do - current_path.should == edit_profile_password_path + expect(current_path).to eq edit_profile_password_path end step 'I should be redirected to account page' do - current_path.should == profile_account_path + expect(current_path).to eq profile_account_path end step 'I click on my profile picture' do - click_link 'profile-pic' + find(:css, '.sidebar-user').click end step 'I should see my user page' do - page.should have_content "User Activity" + expect(page).to have_content "User Activity" - within '.navbar-gitlab' do - page.should have_content current_user.name + page.within '.navbar-gitlab' do + expect(page).to have_content current_user.name end end @@ -187,7 +176,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see groups I belong to' do - page.should have_css('.profile-groups-avatars', visible: true) + expect(page).to have_css('.profile-groups-avatars', visible: true) end step 'I click on new application button' do @@ -195,7 +184,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see application form' do - page.should have_content "New application" + expect(page).to have_content "New Application" end step 'I fill application form out and submit' do @@ -205,9 +194,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I see application' do - page.should have_content "Application: test" - page.should have_content "Application Id" - page.should have_content "Secret" + expect(page).to have_content "Application: test" + expect(page).to have_content "Application Id" + expect(page).to have_content "Secret" end step 'I click edit' do @@ -215,28 +204,28 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I see edit application form' do - page.should have_content "Edit application" + expect(page).to have_content "Edit application" end step 'I change name of application and submit' do - page.should have_content "Edit application" + expect(page).to have_content "Edit application" fill_in :doorkeeper_application_name, with: 'test_changed' click_on "Submit" end step 'I see that application was changed' do - page.should have_content "test_changed" - page.should have_content "Application Id" - page.should have_content "Secret" + expect(page).to have_content "test_changed" + expect(page).to have_content "Application Id" + expect(page).to have_content "Secret" end step 'I click to remove application' do - within '.oauth-applications' do + page.within '.oauth-applications' do click_on "Destroy" end end step "I see that application is removed" do - page.find(".oauth-applications").should_not have_content "test_changed" + expect(page.find(".oauth-applications")).not_to have_content "test_changed" end end diff --git a/features/steps/profile/ssh_keys.rb b/features/steps/profile/ssh_keys.rb index ea912e5b4d..c7f879d247 100644 --- a/features/steps/profile/ssh_keys.rb +++ b/features/steps/profile/ssh_keys.rb @@ -3,7 +3,7 @@ class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps step 'I should see my ssh keys' do @user.keys.each do |key| - page.should have_content(key.title) + expect(page).to have_content(key.title) end end @@ -19,9 +19,9 @@ class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps step 'I should see new ssh key "Laptop"' do key = Key.find_by(title: "Laptop") - page.should have_content(key.title) - page.should have_content(key.key) - current_path.should == profile_key_path(key) + expect(page).to have_content(key.title) + expect(page).to have_content(key.key) + expect(current_path).to eq profile_key_path(key) end step 'I click link "Work"' do @@ -37,7 +37,7 @@ class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps end step 'I should not see "Work" ssh key' do - page.should_not have_content "Work" + expect(page).not_to have_content "Work" end step 'I have ssh key "ssh-rsa Work"' do diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index dd3215adb1..9e96fa5ba4 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -20,8 +20,8 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'I click the "Edit" tab' do - within '.project-settings-nav' do - click_link('Project') + page.within '.sidebar-subnav' do + click_link('Project Settings') end end diff --git a/features/steps/project/archived.rb b/features/steps/project/archived.rb index 37ad0c7765..db1387763d 100644 --- a/features/steps/project/archived.rb +++ b/features/steps/project/archived.rb @@ -19,11 +19,11 @@ class Spinach::Features::ProjectArchived < Spinach::FeatureSteps end step 'I should not see "Archived"' do - page.should_not have_content "Archived" + expect(page).not_to have_content "Archived" end step 'I should see "Archived"' do - page.should have_content "Archived" + expect(page).to have_content "Archived" end When 'I set project archived' do diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 07f7e5796a..338f5e8d3e 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -8,8 +8,8 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps end step 'I should see "Shop" all branches list' do - page.should have_content "Branches" - page.should have_content "master" + expect(page).to have_content "Branches" + expect(page).to have_content "master" end step 'I click link "Protected"' do @@ -17,9 +17,9 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps end step 'I should see "Shop" protected branches list' do - within ".protected-branches-list" do - page.should have_content "stable" - page.should_not have_content "master" + page.within ".protected-branches-list" do + expect(page).to have_content "stable" + expect(page).not_to have_content "master" end end @@ -57,29 +57,29 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps end step 'I should see new branch created' do - page.should have_content 'deploy_keys' + expect(page).to have_content 'deploy_keys' end step 'I should see new an error that branch is invalid' do - page.should have_content 'Branch name invalid' + expect(page).to have_content 'Branch name invalid' end step 'I should see new an error that ref is invalid' do - page.should have_content 'Invalid reference name' + expect(page).to have_content 'Invalid reference name' end step 'I should see new an error that branch already exists' do - page.should have_content 'Branch already exists' + expect(page).to have_content 'Branch already exists' end step "I click branch 'improve/awesome' delete link" do - within '.js-branch-improve\/awesome' do + page.within '.js-branch-improve\/awesome' do find('.btn-remove').click sleep 0.05 end end step "I should not see branch 'improve/awesome'" do - all(visible: true).should_not have_content 'improve/awesome' + expect(page.all(visible: true)).not_to have_content 'improve/awesome' end end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 57b727f837..a8532cc18d 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -2,25 +2,26 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths + include SharedDiffNote include RepoHelpers step 'I see project commits' do commit = @project.repository.commit - page.should have_content(@project.name) - page.should have_content(commit.message[0..20]) - page.should have_content(commit.short_id) + expect(page).to have_content(@project.name) + expect(page).to have_content(commit.message[0..20]) + expect(page).to have_content(commit.short_id) end step 'I click atom feed link' do - click_link "Feed" + click_link "Commits Feed" end step 'I see commits atom feed' do commit = @project.repository.commit - response_headers['Content-Type'].should have_content("application/atom+xml") - body.should have_selector("title", text: "Recent commits to #{@project.name}") - body.should have_selector("author email", text: commit.author_email) - body.should have_selector("entry summary", text: commit.description[0..10]) + expect(response_headers['Content-Type']).to have_content("application/atom+xml") + expect(body).to have_selector("title", text: "#{@project.name}:master commits") + expect(body).to have_selector("author email", text: commit.author_email) + expect(body).to have_selector("entry summary", text: commit.description[0..10]) end step 'I click on commit link' do @@ -28,8 +29,8 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I see commit info' do - page.should have_content sample_commit.message - page.should have_content "Showing #{sample_commit.files_changed_count} changed files" + expect(page).to have_content sample_commit.message + expect(page).to have_content "Showing #{sample_commit.files_changed_count} changed files" end step 'I fill compare fields with refs' do @@ -45,38 +46,43 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I should see additional file lines' do - within @diff.parent do - first('.new_line').text.should_not have_content "..." + page.within @diff.parent do + expect(first('.new_line').text).not_to have_content "..." end end step 'I see compared refs' do - page.should have_content "Compare View" - page.should have_content "Commits (1)" - page.should have_content "Showing 2 changed files" + expect(page).to have_content "Compare View" + expect(page).to have_content "Commits (1)" + expect(page).to have_content "Showing 2 changed files" end step 'I see breadcrumb links' do - page.should have_selector('ul.breadcrumb') - page.should have_selector('ul.breadcrumb a', count: 4) + expect(page).to have_selector('ul.breadcrumb') + expect(page).to have_selector('ul.breadcrumb a', count: 4) end step 'I see commits stats' do - page.should have_content 'Top 50 Committers' - page.should have_content 'Committers' - page.should have_content 'Total commits' - page.should have_content 'Authors' + expect(page).to have_content 'Top 50 Committers' + expect(page).to have_content 'Committers' + expect(page).to have_content 'Total commits' + expect(page).to have_content 'Authors' end step 'I visit big commit page' do - Commit::DIFF_SAFE_FILES = 20 + stub_const('Commit::DIFF_SAFE_FILES', 20) visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id) end step 'I see big commit warning' do - page.should have_content sample_big_commit.message - page.should have_content "Too many changes" - Commit::DIFF_SAFE_FILES = 100 + expect(page).to have_content sample_big_commit.message + expect(page).to have_content "Too many changes" + end + + step 'I see "Reload with full diff" link' do + link = find_link('Reload with full diff') + expect(link[:href]).to end_with('?force_show_diff=true') + expect(link[:href]).not_to include('.html') end step 'I visit a commit with an image that changed' do @@ -84,20 +90,12 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'The diff links to both the previous and current image' do - links = all('.two-up span div a') - links[0]['href'].should =~ %r{blob/#{sample_image_commit.old_blob_id}} - links[1]['href'].should =~ %r{blob/#{sample_image_commit.new_blob_id}} - end - - step 'I click side-by-side diff button' do - click_link "Side-by-side" - end - - step 'I see side-by-side diff button' do - page.should have_content "Side-by-side" + links = page.all('.two-up span div a') + expect(links[0]['href']).to match %r{blob/#{sample_image_commit.old_blob_id}} + expect(links[1]['href']).to match %r{blob/#{sample_image_commit.new_blob_id}} end step 'I see inline diff button' do - page.should have_content "Inline" + expect(page).to have_content "Inline" end end diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb index 3465fcbfd0..e6f8faf50f 100644 --- a/features/steps/project/commits/tags.rb +++ b/features/steps/project/commits/tags.rb @@ -4,8 +4,8 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps include SharedPaths step 'I should see "Shop" all tags list' do - page.should have_content "Tags" - page.should have_content "v1.0.0" + expect(page).to have_content "Tags" + expect(page).to have_content "v1.0.0" end step 'I click new tag link' do @@ -37,37 +37,37 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps end step 'I should see new tag created' do - page.should have_content 'v7.0' + expect(page).to have_content 'v7.0' end step 'I should see new an error that tag is invalid' do - page.should have_content 'Tag name invalid' + expect(page).to have_content 'Tag name invalid' end step 'I should see new an error that tag ref is invalid' do - page.should have_content 'Invalid reference name' + expect(page).to have_content 'Invalid reference name' end step 'I should see new an error that tag already exists' do - page.should have_content 'Tag already exists' + expect(page).to have_content 'Tag already exists' end step "I delete tag 'v1.1.0'" do - within '.tags' do + page.within '.tags' do first('.btn-remove').click sleep 0.05 end end step "I should not see tag 'v1.1.0'" do - within '.tags' do - all(visible: true).should_not have_content 'v1.1.0' + page.within '.tags' do + expect(page.all(visible: true)).not_to have_content 'v1.1.0' end end step 'I delete all tags' do - within '.tags' do - all('.btn-remove').each do |remove| + page.within '.tags' do + page.all('.btn-remove').each do |remove| remove.click sleep 0.05 end @@ -75,8 +75,8 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps end step 'I should see tags info message' do - within '.tags' do - page.should have_content 'Repository has no tags yet.' + page.within '.tags' do + expect(page).to have_content 'Repository has no tags yet.' end end end diff --git a/features/steps/project/commits/user_lookup.rb b/features/steps/project/commits/user_lookup.rb index 63ff84c82e..40cada6da4 100644 --- a/features/steps/project/commits/user_lookup.rb +++ b/features/steps/project/commits/user_lookup.rb @@ -29,9 +29,9 @@ class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps def check_author_link(email, user) author_link = find('.commit-author-link') - author_link['href'].should == user_path(user) - author_link['data-original-title'].should == email - find('.commit-author-name').text.should == user.name + expect(author_link['href']).to eq user_path(user) + expect(author_link['data-original-title']).to eq email + expect(find('.commit-author-name').text).to eq user.name end def user_primary diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index 6b85cf74f5..0d39e1997b 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -8,20 +8,20 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps end step 'I should see project page' do - page.should have_content "Empty" - current_path.should == namespace_project_path(Project.last.namespace, Project.last) + expect(page).to have_content "Empty" + expect(current_path).to eq namespace_project_path(Project.last.namespace, Project.last) end step 'I should see empty project instuctions' do - page.should have_content "git init" - page.should have_content "git remote" - page.should have_content Project.last.url_to_repo + expect(page).to have_content "git init" + expect(page).to have_content "git remote" + expect(page).to have_content Project.last.url_to_repo end step 'I see empty project instuctions' do - page.should have_content "git init" - page.should have_content "git remote" - page.should have_content Project.last.url_to_repo + expect(page).to have_content "git init" + expect(page).to have_content "git remote" + expect(page).to have_content Project.last.url_to_repo end step 'I click on HTTP' do @@ -29,7 +29,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps end step 'Remote url should update to http link' do - page.should have_content "git remote add origin #{Project.last.http_url_to_repo}" + expect(page).to have_content "git remote add origin #{Project.last.http_url_to_repo}" end step 'If I click on SSH' do @@ -37,6 +37,6 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps end step 'Remote url should update to ssh link' do - page.should have_content "git remote add origin #{Project.last.url_to_repo}" + expect(page).to have_content "git remote add origin #{Project.last.url_to_repo}" end end diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index 50e14513a7..a4d6c9a1b8 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -8,20 +8,20 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I should see project deploy key' do - within '.enabled-keys' do - page.should have_content deploy_key.title + page.within '.enabled-keys' do + expect(page).to have_content deploy_key.title end end step 'I should see other project deploy key' do - within '.available-keys' do - page.should have_content other_deploy_key.title + page.within '.available-keys' do + expect(page).to have_content other_deploy_key.title end end step 'I should see public deploy key' do - within '.available-keys' do - page.should have_content public_deploy_key.title + page.within '.available-keys' do + expect(page).to have_content public_deploy_key.title end end @@ -36,19 +36,29 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I should be on deploy keys page' do - current_path.should == namespace_project_deploy_keys_path(@project.namespace, @project) + expect(current_path).to eq namespace_project_deploy_keys_path(@project.namespace, @project) end step 'I should see newly created deploy key' do - within '.enabled-keys' do - page.should have_content(deploy_key.title) + page.within '.enabled-keys' do + expect(page).to have_content(deploy_key.title) end end - step 'other project has deploy key' do - @second_project = create :project, namespace: create(:group) + step 'other projects have deploy keys' do + @second_project = create(:project, namespace: create(:group)) @second_project.team << [current_user, :master] create(:deploy_keys_project, project: @second_project) + + @third_project = create(:project, namespace: create(:group)) + @third_project.team << [current_user, :master] + create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first) + end + + step 'I should only see the same deploy key once' do + page.within '.available-keys' do + expect(page).to have_selector('ul li', count: 1) + end end step 'public deploy key exists' do @@ -56,7 +66,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I click attach deploy key' do - within '.available-keys' do + page.within '.available-keys' do click_link 'Enable' end end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 8e58597db2..0e433781d7 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -4,8 +4,8 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps include SharedProject step 'I click link "Fork"' do - page.should have_content "Shop" - page.should have_content "Fork" + expect(page).to have_content "Shop" + expect(page).to have_content "Fork" click_link "Fork" end @@ -15,7 +15,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I should see the forked project page' do - page.should have_content "Project was successfully forked." + expect(page).to have_content "Project was successfully forked." end step 'I already have a project named "Shop" in my namespace' do @@ -23,11 +23,11 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I should see a "Name has already been taken" warning' do - page.should have_content "Name has already been taken" + expect(page).to have_content "Name has already been taken" end step 'I fork to my namespace' do - within '.fork-namespaces' do + page.within '.fork-namespaces' do click_link current_user.name end end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 63ad90e124..3e97e84d11 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -21,17 +21,17 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I should see merge request "Merge Request On Forked Project"' do - @project.merge_requests.size.should >= 1 + expect(@project.merge_requests.size).to be >= 1 @merge_request = @project.merge_requests.last - current_path.should == namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - @merge_request.title.should == "Merge Request On Forked Project" - @merge_request.source_project.should == @forked_project - @merge_request.source_branch.should == "fix" - @merge_request.target_branch.should == "master" - page.should have_content @forked_project.path_with_namespace - page.should have_content @project.path_with_namespace - page.should have_content @merge_request.source_branch - page.should have_content @merge_request.target_branch + expect(current_path).to eq namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + expect(@merge_request.title).to eq "Merge Request On Forked Project" + expect(@merge_request.source_project).to eq @forked_project + expect(@merge_request.source_branch).to eq "fix" + expect(@merge_request.target_branch).to eq "master" + expect(page).to have_content @forked_project.path_with_namespace + expect(page).to have_content @project.path_with_namespace + expect(page).to have_content @merge_request.source_branch + expect(page).to have_content @merge_request.target_branch end step 'I fill out a "Merge Request On Forked Project" merge request' do @@ -42,11 +42,12 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps click_button "Compare branches" + expect(page).to have_content "New merge request" fill_in "merge_request_title", with: "Merge Request On Forked Project" end step 'I submit the merge request' do - click_button "Submit merge request" + click_button "Submit new merge request" end step 'I follow the target commit link' do @@ -56,7 +57,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps step 'I should see the commit under the forked from project' do commit = @project.repository.commit - page.should have_content(commit.message) + expect(page).to have_content(commit.message) end step 'I click "Create Merge Request on fork" link' do @@ -64,12 +65,12 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I see prefilled new Merge Request page for the forked project' do - current_path.should == new_namespace_project_merge_request_path(@forked_project.namespace, @forked_project) - find("#merge_request_source_project_id").value.should == @forked_project.id.to_s - find("#merge_request_target_project_id").value.should == @project.id.to_s - find("#merge_request_source_branch").value.should have_content "new_design" - find("#merge_request_target_branch").value.should have_content "master" - find("#merge_request_title").value.should == "New Design" + expect(current_path).to eq new_namespace_project_merge_request_path(@forked_project.namespace, @forked_project) + expect(find("#merge_request_source_project_id").value).to eq @forked_project.id.to_s + expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s + expect(find("#merge_request_source_branch").value).to have_content "new_design" + expect(find("#merge_request_target_branch").value).to have_content "master" + expect(find("#merge_request_title").value).to eq "New Design" verify_commit_link(".mr_target_commit", @project) verify_commit_link(".mr_source_commit", @forked_project) end @@ -83,22 +84,22 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I should see the edited merge request' do - page.should have_content "An Edited Forked Merge Request" - @project.merge_requests.size.should >= 1 + expect(page).to have_content "An Edited Forked Merge Request" + expect(@project.merge_requests.size).to be >= 1 @merge_request = @project.merge_requests.last - current_path.should == namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - @merge_request.source_project.should == @forked_project - @merge_request.source_branch.should == "fix" - @merge_request.target_branch.should == "master" - page.should have_content @forked_project.path_with_namespace - page.should have_content @project.path_with_namespace - page.should have_content @merge_request.source_branch - page.should have_content @merge_request.target_branch + expect(current_path).to eq namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + expect(@merge_request.source_project).to eq @forked_project + expect(@merge_request.source_branch).to eq "fix" + expect(@merge_request.target_branch).to eq "master" + expect(page).to have_content @forked_project.path_with_namespace + expect(page).to have_content @project.path_with_namespace + expect(page).to have_content @merge_request.source_branch + expect(page).to have_content @merge_request.target_branch end step 'I should see last push widget' do - page.should have_content "You pushed to new_design" - page.should have_link "Create Merge Request" + expect(page).to have_content "You pushed to new_design" + expect(page).to have_link "Create Merge Request" end step 'I click link edit "Merge Request On Forked Project"' do @@ -106,31 +107,47 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I see the edit page prefilled for "Merge Request On Forked Project"' do - current_path.should == edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - page.should have_content "Edit merge request ##{@merge_request.id}" - find("#merge_request_title").value.should == "Merge Request On Forked Project" + expect(current_path).to eq edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + expect(page).to have_content "Edit merge request ##{@merge_request.id}" + expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project" end step 'I fill out an invalid "Merge Request On Forked Project" merge request' do select "Select branch", from: "merge_request_target_branch" - find(:select, "merge_request_source_project_id", {}).value.should == @forked_project.id.to_s - find(:select, "merge_request_target_project_id", {}).value.should == @project.id.to_s - find(:select, "merge_request_source_branch", {}).value.should == "" - find(:select, "merge_request_target_branch", {}).value.should == "" + expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s + expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s + expect(find(:select, "merge_request_source_branch", {}).value).to eq "" + expect(find(:select, "merge_request_target_branch", {}).value).to eq "" click_button "Compare branches" end step 'I should see validation errors' do - page.should have_content "You must select source and target branch" + expect(page).to have_content "You must select source and target branch" end step 'the target repository should be the original repository' do - page.should have_select("merge_request_target_project_id", selected: @project.path_with_namespace) + expect(page).to have_select("merge_request_target_project_id", selected: @project.path_with_namespace) + end + + step 'I click "Assign to" dropdown"' do + first('.ajax-users-select').click + end + + step 'I should see the target project ID in the input selector' do + expect(page).to have_selector("input[data-project-id=\"#{@project.id}\"]") + end + + step 'I should see the users from the target project ID' do + expect(page).to have_selector('.user-result', visible: true, count: 3) + users = page.all('.user-name') + expect(users[0].text).to eq 'Unassigned' + expect(users[1].text).to eq current_user.name + expect(users[2].text).to eq @project.users.first.name end # Verify a link is generated against the correct project def verify_commit_link(container_div, container_project) # This should force a wait for the javascript to execute - find(:div,container_div).find(".commit_short_id")['href'].should have_content "#{container_project.path_with_namespace}/commit" + expect(find(:div,container_div).find(".commit_short_id")['href']).to have_content "#{container_project.path_with_namespace}/commit" end end diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index a2807c340f..5e7e573a6a 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -3,7 +3,7 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps include SharedProject step 'page should have graphs' do - page.should have_selector ".stat-graph" + expect(page).to have_selector ".stat-graph" end When 'I visit project "Shop" graph page' do @@ -17,7 +17,7 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps end step 'page should have commits graphs' do - page.should have_content "Commit statistics for master" - page.should have_content "Commits per day of month" + expect(page).to have_content "Commit statistics for master" + expect(page).to have_content "Commits per day of month" end end diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index 4b13520259..04e3bf78ed 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -19,18 +19,18 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps end step 'I should see project hook' do - page.should have_content @hook.url + expect(page).to have_content @hook.url end step 'I submit new hook' do - @url = Faker::Internet.uri("http") + @url = FFaker::Internet.uri("http") fill_in "hook_url", with: @url expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1) end step 'I should see newly created hook' do - current_path.should == namespace_project_hooks_path(current_project.namespace, current_project) - page.should have_content(@url) + expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project) + expect(page).to have_content(@url) end step 'I click test hook button' do @@ -44,19 +44,19 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps end step 'hook should be triggered' do - current_path.should == namespace_project_hooks_path(current_project.namespace, current_project) - page.should have_selector '.flash-notice', + expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project) + expect(page).to have_selector '.flash-notice', text: 'Hook successfully executed.' end step 'I should see hook error message' do - page.should have_selector '.flash-alert', + expect(page).to have_selector '.flash-alert', text: 'Hook execution failed. '\ 'Ensure the project has commits.' end step 'I should see hook service down error message' do - page.should have_selector '.flash-alert', + expect(page).to have_selector '.flash-alert', text: 'Hook execution failed. '\ 'Ensure hook URL is correct and '\ 'service is up.' diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index 5740bd1283..50bb32429b 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -5,26 +5,26 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps include Select2Helper step 'I should see "Bugfix1" in issues list' do - within ".issues-list" do - page.should have_content "Bugfix1" + page.within ".issues-list" do + expect(page).to have_content "Bugfix1" end end step 'I should see "Bugfix2" in issues list' do - within ".issues-list" do - page.should have_content "Bugfix2" + page.within ".issues-list" do + expect(page).to have_content "Bugfix2" end end step 'I should not see "Bugfix2" in issues list' do - within ".issues-list" do - page.should_not have_content "Bugfix2" + page.within ".issues-list" do + expect(page).not_to have_content "Bugfix2" end end step 'I should not see "Feature1" in issues list' do - within ".issues-list" do - page.should_not have_content "Feature1" + page.within ".issues-list" do + expect(page).not_to have_content "Feature1" end end @@ -33,7 +33,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps end step 'I click link "feature"' do - within ".labels-filter" do + page.within ".labels-filter" do click_link "feature" end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index b8e282b202..239392eab9 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -7,24 +7,23 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps include SharedMarkdown step 'I should see "Release 0.4" in issues' do - page.should have_content "Release 0.4" + expect(page).to have_content "Release 0.4" end step 'I should not see "Release 0.3" in issues' do - page.should_not have_content "Release 0.3" + expect(page).not_to have_content "Release 0.3" end step 'I should not see "Tweet control" in issues' do - page.should_not have_content "Tweet control" + expect(page).not_to have_content "Tweet control" end step 'I should see that I am subscribed' do - find(".subscribe-button span").text.should == "Unsubscribe" + expect(find('.subscribe-button span')).to have_content 'Unsubscribe' end step 'I should see that I am unsubscribed' do - sleep 0.2 - find(".subscribe-button span").text.should == "Subscribe" + expect(find('.subscribe-button span')).to have_content 'Subscribe' end step 'I click link "Closed"' do @@ -36,11 +35,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see "Release 0.3" in issues' do - page.should have_content "Release 0.3" + expect(page).to have_content "Release 0.3" end step 'I should not see "Release 0.4" in issues' do - page.should_not have_content "Release 0.4" + expect(page).not_to have_content "Release 0.4" end step 'I click link "All"' do @@ -52,7 +51,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see issue "Release 0.4"' do - page.should have_content "Release 0.4" + expect(page).to have_content "Release 0.4" end step 'I click link "New Issue"' do @@ -66,9 +65,9 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I see current user as the first user' do expect(page).to have_selector('.user-result', visible: true, count: 4) users = page.all('.user-name') - users[0].text.should == 'Any' - users[1].text.should == 'Unassigned' - users[2].text.should == current_user.name + expect(users[0].text).to eq 'Any' + expect(users[1].text).to eq 'Unassigned' + expect(users[2].text).to eq current_user.name end step 'I submit new issue "500 error on profile"' do @@ -87,16 +86,16 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see label \'bug\' with issue' do - within '.issue-show-labels' do - page.should have_content 'bug' + page.within '.issue-show-labels' do + expect(page).to have_content 'bug' end end step 'I should see issue "500 error on profile"' do issue = Issue.find_by(title: "500 error on profile") - page.should have_content issue.title - page.should have_content issue.author_name - page.should have_content issue.project.name + expect(page).to have_content issue.title + expect(page).to have_content issue.author_name + expect(page).to have_content issue.project.name end step 'I fill in issue search with "Re"' do @@ -139,7 +138,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I should see selected milestone with title "v3.0"' do issues_milestone_selector = "#issue_milestone_id_chzn > a" - find(issues_milestone_selector).should have_content("v3.0") + expect(find(issues_milestone_selector)).to have_content("v3.0") end When 'I select first assignee from "Shop" project' do @@ -152,7 +151,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps issues_assignee_selector = "#issue_assignee_id_chzn > a" assignee_name = project.users.first.name - find(issues_assignee_selector).should have_content(assignee_name) + expect(find(issues_assignee_selector)).to have_content(assignee_name) end step 'project "Shop" have "Release 0.4" open issue' do @@ -179,14 +178,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps author: project.users.first) end - step 'project "Shop" has "Tasks-open" open issue with task markdown' do - create_taskable(:issue, 'Tasks-open') - end - - step 'project "Shop" has "Tasks-closed" closed issue with task markdown' do - create_taskable(:closed_issue, 'Tasks-closed') - end - step 'empty project "Empty Project"' do create :empty_project, name: 'Empty Project', namespace: @user.namespace end @@ -198,18 +189,23 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I see empty project details with ssh clone info' do project = Project.find_by(name: 'Empty Project') - all(:css, '.git-empty .clone').each do |element| - element.text.should include(project.url_to_repo) + page.all(:css, '.git-empty .clone').each do |element| + expect(element.text).to include(project.url_to_repo) end end + When "I visit project \"Community\" issues page" do + project = Project.find_by(name: 'Community') + visit namespace_project_issues_path(project.namespace, project) + end + When "I visit empty project's issues page" do project = Project.find_by(name: 'Empty Project') visit namespace_project_issues_path(project.namespace, project) end step 'I leave a comment with code block' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do fill_in "note[note]", with: "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```" click_button "Add Comment" sleep 0.05 @@ -217,13 +213,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see an error alert section within the comment form' do - within(".js-main-target-form") do + page.within(".js-main-target-form") do find(".error-alert") end end step 'The code block should be unchanged' do - page.should have_content("```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```") + expect(page).to have_content("```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```") end step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do @@ -247,15 +243,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see \'Bugfix1\' in issues' do - page.should have_content 'Bugfix1' + expect(page).to have_content 'Bugfix1' end step 'I should see \'Feature1\' in issues' do - page.should have_content 'Feature1' + expect(page).to have_content 'Feature1' end step 'I should not see \'Bugfix1\' in issues' do - page.should_not have_content 'Bugfix1' + expect(page).not_to have_content 'Bugfix1' end step 'issue \'Release 0.4\' has label \'bug\'' do @@ -265,11 +261,29 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click label \'bug\'' do - within ".issues-list" do + page.within ".issues-list" do click_link 'bug' end end + step 'I should not see labels field' do + page.within '.issue-form' do + expect(page).not_to have_content("Labels") + end + end + + step 'I should not see milestone field' do + page.within '.issue-form' do + expect(page).not_to have_content("Milestone") + end + end + + step 'I should not see assignee field' do + page.within '.issue-form' do + expect(page).not_to have_content("Assign to") + end + end + def filter_issue(text) fill_in 'issue_search', with: text end diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 6ce34c500c..d656acf422 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -8,14 +8,14 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I remove label \'bug\'' do - within "#label_#{bug_label.id}" do + page.within "#label_#{bug_label.id}" do click_link 'Remove' end end step 'I delete all labels' do - within '.labels' do - all('.btn-remove').each do |remove| + page.within '.labels' do + page.all('.btn-remove').each do |remove| remove.click sleep 0.05 end @@ -23,8 +23,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see labels help message' do - within '.labels' do - page.should have_content 'Create first label or generate default set of '\ + page.within '.labels' do + expect(page).to have_content 'Create first label or generate default set of '\ 'labels' end end @@ -48,38 +48,38 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see label label exist error message' do - within '.label-form' do - page.should have_content 'Title has already been taken' + page.within '.label-form' do + expect(page).to have_content 'Title has already been taken' end end step 'I should see label color error message' do - within '.label-form' do - page.should have_content 'Color is invalid' + page.within '.label-form' do + expect(page).to have_content 'Color is invalid' end end step 'I should see label \'feature\'' do - within '.manage-labels-list' do - page.should have_content 'feature' + page.within '.manage-labels-list' do + expect(page).to have_content 'feature' end end step 'I should see label \'bug\'' do - within '.manage-labels-list' do - page.should have_content 'bug' + page.within '.manage-labels-list' do + expect(page).to have_content 'bug' end end step 'I should not see label \'bug\'' do - within '.manage-labels-list' do - page.should_not have_content 'bug' + page.within '.manage-labels-list' do + expect(page).not_to have_content 'bug' end end step 'I should see label \'support\'' do - within '.manage-labels-list' do - page.should have_content 'support' + page.within '.manage-labels-list' do + expect(page).to have_content 'support' end end @@ -90,8 +90,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see label \'fix\'' do - within '.manage-labels-list' do - page.should have_content 'fix' + page.within '.manage-labels-list' do + expect(page).to have_content 'fix' end end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index cce87a6d98..61e62c2adb 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -6,9 +6,9 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps step 'I should see milestone "v2.2"' do milestone = @project.milestones.find_by(title: "v2.2") - page.should have_content(milestone.title[0..10]) - page.should have_content(milestone.expires_at) - page.should have_content("Issues") + expect(page).to have_content(milestone.title[0..10]) + expect(page).to have_content(milestone.expires_at) + expect(page).to have_content("Issues") end step 'I click link "v2.2"' do @@ -26,9 +26,9 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps step 'I should see milestone "v2.3"' do milestone = @project.milestones.find_by(title: "v2.3") - page.should have_content(milestone.title[0..10]) - page.should have_content(milestone.expires_at) - page.should have_content("Issues") + expect(page).to have_content(milestone.title[0..10]) + expect(page).to have_content(milestone.expires_at) + expect(page).to have_content("Issues") end step 'project "Shop" has milestone "v2.2"' do @@ -54,6 +54,14 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps end step 'I should see 3 issues' do - page.should have_selector('#tab-issues li.issue-row', count: 4) + expect(page).to have_selector('#tab-issues li.issue-row', count: 4) + end + + step 'I click link to remove milestone "v2.2"' do + click_link 'Remove' + end + + step 'I should see no milestones' do + expect(page).to have_content('No milestones to show') end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index bb1f9f129c..04784207a1 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -6,6 +6,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps include SharedPaths include SharedMarkdown include SharedDiffNote + include SharedUser step 'I click link "New Merge Request"' do click_link "New Merge Request" @@ -24,44 +25,44 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see merge request "Wiki Feature"' do - within '.merge-request' do - page.should have_content "Wiki Feature" + page.within '.merge-request' do + expect(page).to have_content "Wiki Feature" end end step 'I should see closed merge request "Bug NS-04"' do merge_request = MergeRequest.find_by!(title: "Bug NS-04") - merge_request.closed?.should be_true - page.should have_content "Closed by" + expect(merge_request).to be_closed + expect(page).to have_content "Closed by" end step 'I should see merge request "Bug NS-04"' do - page.should have_content "Bug NS-04" + expect(page).to have_content "Bug NS-04" end step 'I should see "Bug NS-04" in merge requests' do - page.should have_content "Bug NS-04" + expect(page).to have_content "Bug NS-04" end step 'I should see "Feature NS-03" in merge requests' do - page.should have_content "Feature NS-03" + expect(page).to have_content "Feature NS-03" end step 'I should not see "Feature NS-03" in merge requests' do - page.should_not have_content "Feature NS-03" + expect(page).not_to have_content "Feature NS-03" end step 'I should not see "Bug NS-04" in merge requests' do - page.should_not have_content "Bug NS-04" + expect(page).not_to have_content "Bug NS-04" end step 'I should see that I am subscribed' do - find(".subscribe-button span").text.should == "Unsubscribe" + expect(find('.subscribe-button span')).to have_content 'Unsubscribe' end step 'I should see that I am unsubscribed' do - find(".subscribe-button span").should have_content("Subscribe") + expect(find('.subscribe-button span')).to have_content 'Subscribe' end step 'I click button "Unsubscribe"' do @@ -77,7 +78,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps select "feature", from: "merge_request_target_branch" click_button "Compare branches" fill_in "merge_request_title", with: "Wiki Feature" - click_button "Submit merge request" + click_button "Submit new merge request" end step 'project "Shop" have "Bug NS-04" open merge request' do @@ -108,26 +109,26 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps author: project.users.first) end - step 'project "Shop" has "MR-task-open" open MR with task markdown' do - create_taskable(:merge_request, 'MR-task-open') + step 'project "Community" has "Bug CO-01" open merge request with diffs inside' do + project = Project.find_by(name: "Community") + create(:merge_request_with_diffs, + title: "Bug CO-01", + source_project: project, + target_project: project, + author: project.users.first) end - step 'I switch to the diff tab' do - visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) - end + step 'I click on the Changes tab' do + page.within '.merge-request-tabs' do + click_link 'Changes' + end - step 'I click on the Changes tab via Javascript' do - find('.diffs-tab').click - sleep 2 + # Waits for load + expect(page).to have_css('.tab-content #diffs.active') end step 'I should see the proper Inline and Side-by-side links' do - buttons = all('#commit-diff-viewtype') - expect(buttons.count).to eq(2) - - buttons.each do |b| - expect(b['href']).should_not have_content('json') - end + expect(page).to have_css('#commit-diff-viewtype', count: 2) end step 'I switch to the merge request\'s comments tab' do @@ -135,11 +136,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click on the commit in the merge request' do - within '.merge-request-tabs' do + page.within '.merge-request-tabs' do click_link 'Commits' end - within '.commits' do + page.within '.commits' do click_link Commit.truncate_sha(sample_commit.id) end end @@ -165,24 +166,30 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see a discussion has started on diff' do - page.should have_content "#{current_user.name} started a discussion" - page.should have_content sample_commit.line_code_path - page.should have_content "Line is wrong" + page.within(".notes .discussion") do + page.should have_content "#{current_user.name} started a discussion" + page.should have_content sample_commit.line_code_path + page.should have_content "Line is wrong" + end end step 'I should see a discussion has started on commit diff' do - page.should have_content "#{current_user.name} started a discussion on commit" - page.should have_content sample_commit.line_code_path - page.should have_content "Line is wrong" + page.within(".notes .discussion") do + page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content sample_commit.line_code_path + page.should have_content "Line is wrong" + end end step 'I should see a discussion has started on commit' do - page.should have_content "#{current_user.name} started a discussion on commit" - page.should have_content "One comment to rule them all" + page.within(".notes .discussion") do + page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content "One comment to rule them all" + end end step 'merge request is mergeable' do - page.should have_button 'Accept Merge Request' + expect(page).to have_button 'Accept Merge Request' end step 'I modify merge commit message' do @@ -191,6 +198,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'merge request "Bug NS-05" is mergeable' do + merge_request.project.satellite.create merge_request.mark_as_mergeable end @@ -199,14 +207,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps merge!: true, ) - within '.can_be_merged' do + page.within '.mr-state-widget' do click_button "Accept Merge Request" end end step 'I should see merged request' do - within '.issue-box' do - page.should have_content "Merged" + page.within '.issue-box' do + expect(page).to have_content "Merged" end end @@ -215,76 +223,78 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see reopened merge request "Bug NS-04"' do - within '.issue-box' do - page.should have_content "Open" + page.within '.issue-box' do + expect(page).to have_content "Open" end end step 'I click link "Hide inline discussion" of the second file' do - within '.files [id^=diff]:nth-child(2)' do + page.within '.files [id^=diff]:nth-child(2)' do find('.js-toggle-diff-comments').click end end step 'I click link "Show inline discussion" of the second file' do - within '.files [id^=diff]:nth-child(2)' do + page.within '.files [id^=diff]:nth-child(2)' do find('.js-toggle-diff-comments').click end end step 'I should not see a comment like "Line is wrong" in the second file' do - within '.files [id^=diff]:nth-child(2)' do - page.should_not have_visible_content "Line is wrong" + page.within '.files [id^=diff]:nth-child(2)' do + expect(page).not_to have_visible_content "Line is wrong" end end step 'I should see a comment like "Line is wrong" in the second file' do - within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do - page.should have_visible_content "Line is wrong" + page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do + expect(page).to have_visible_content "Line is wrong" end end step 'I should not see a comment like "Line is wrong here" in the second file' do - within '.files [id^=diff]:nth-child(2)' do - page.should_not have_visible_content "Line is wrong here" + page.within '.files [id^=diff]:nth-child(2)' do + expect(page).not_to have_visible_content "Line is wrong here" end end step 'I should see a comment like "Line is wrong here" in the second file' do - within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do - page.should have_visible_content "Line is wrong here" + page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do + expect(page).to have_visible_content "Line is wrong here" end end step 'I leave a comment like "Line is correct" on line 12 of the first file' do init_diff_note_first_file - within(".js-discussion-note-form") do + page.within(".js-discussion-note-form") do fill_in "note_note", with: "Line is correct" click_button "Add Comment" end - within ".files [id^=diff]:nth-child(1) .note-body > .note-text" do - page.should have_content "Line is correct" + page.within ".files [id^=diff]:nth-child(1) .note-body > .note-text" do + expect(page).to have_content "Line is correct" end end step 'I leave a comment like "Line is wrong" on line 39 of the second file' do init_diff_note_second_file - within(".js-discussion-note-form") do + page.within(".js-discussion-note-form") do fill_in "note_note", with: "Line is wrong on here" click_button "Add Comment" end end step 'I should still see a comment like "Line is correct" in the first file' do - within '.files [id^=diff]:nth-child(1) .note-body > .note-text' do - page.should have_visible_content "Line is correct" + page.within '.files [id^=diff]:nth-child(1) .note-body > .note-text' do + expect(page).to have_visible_content "Line is correct" end end step 'I unfold diff' do + expect(page).to have_css('.js-unfold') + first('.js-unfold').click end @@ -297,8 +307,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see comments on the side-by-side diff page' do - within '.files [id^=diff]:nth-child(1) .parallel .note-body > .note-text' do - page.should have_visible_content "Line is correct" + page.within '.files [id^=diff]:nth-child(1) .parallel .note-body > .note-text' do + expect(page).to have_visible_content "Line is correct" end end @@ -306,6 +316,32 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps fill_in 'issue_search', with: "Fe" end + step 'I click the "Target branch" dropdown' do + first('.target_branch').click + end + + step 'I select a new target branch' do + select "feature", from: "merge_request_target_branch" + click_button 'Save' + end + + step 'I should see new target branch changes' do + expect(page).to have_content 'Request to merge fix into feature' + expect(page).to have_content 'Target branch changed from master to feature' + end + + step 'I click on "Email Patches"' do + click_link "Email Patches" + end + + step 'I click on "Plain Diff"' do + click_link "Plain Diff" + end + + step 'I should see a patch diff' do + expect(page).to have_content('diff --git') + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end @@ -315,12 +351,13 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end def leave_comment(message) - within(".js-discussion-note-form") do + page.within(".js-discussion-note-form", visible: true) do fill_in "note_note", with: message click_button "Add Comment" end - - page.should have_content message + page.within(".notes_holder", visible: true) do + expect(page).to have_content message + end end def init_diff_note_first_file diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index a15688ace6..7a83d32a24 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -4,28 +4,38 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps include SharedProject step 'page should have network graph' do - page.should have_selector ".network-graph" + expect(page).to have_selector ".network-graph" end When 'I visit project "Shop" network page' do # Stub Graph max_size to speed up test (10 commits vs. 650) Network::Graph.stub(max_count: 10) - project = Project.find_by(name: "Shop") - visit namespace_project_network_path(project.namespace, project, "master") + @project = Project.find_by(name: "Shop") + visit namespace_project_network_path(@project.namespace, @project, "master") + end + + step "I visit project network page on branch 'test'" do + visit namespace_project_network_path(@project.namespace, @project, "'test'") end step 'page should select "master" in select box' do - page.should have_selector '.select2-chosen', text: "master" + expect(page).to have_selector '.select2-chosen', text: "master" end step 'page should select "v1.0.0" in select box' do - page.should have_selector '.select2-chosen', text: "v1.0.0" + expect(page).to have_selector '.select2-chosen', text: "v1.0.0" end step 'page should have "master" on graph' do - within '.network-graph' do - page.should have_content 'master' + page.within '.network-graph' do + expect(page).to have_content 'master' + end + end + + step "page should have 'test' on graph" do + page.within '.network-graph' do + expect(page).to have_content "'test'" end end @@ -45,33 +55,33 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should have content not containing "v1.0.0"' do - within '.network-graph' do - page.should have_content 'Change some files' + page.within '.network-graph' do + expect(page).to have_content 'Change some files' end end step 'page should not have content not containing "v1.0.0"' do - within '.network-graph' do - page.should_not have_content 'Change some files' + page.within '.network-graph' do + expect(page).not_to have_content 'Change some files' end end step 'page should select "feature" in select box' do - page.should have_selector '.select2-chosen', text: "feature" + expect(page).to have_selector '.select2-chosen', text: "feature" end step 'page should select "v1.0.0" in select box' do - page.should have_selector '.select2-chosen', text: "v1.0.0" + expect(page).to have_selector '.select2-chosen', text: "v1.0.0" end step 'page should have "feature" on graph' do - within '.network-graph' do - page.should have_content 'feature' + page.within '.network-graph' do + expect(page).to have_content 'feature' end end When 'I looking for a commit by SHA of "v1.0.0"' do - within ".network-form" do + page.within ".network-form" do fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' find('button').click end @@ -79,13 +89,13 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should have "v1.0.0" on graph' do - within '.network-graph' do - page.should have_content 'v1.0.0' + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' end end When 'I look for a commit by ";"' do - within ".network-form" do + page.within ".network-form" do fill_in 'extended_sha1', with: ';' find('button').click end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index d39c8e7d2d..0404fd5e59 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -13,7 +13,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project with new settings' do - find_field('project_name').value.should == 'NewName' + expect(find_field('project_name').value).to eq 'NewName' end step 'change project path settings' do @@ -22,32 +22,32 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project with new path settings' do - project.path.should == 'new-path' + expect(project.path).to eq 'new-path' end step 'I change the project avatar' do attach_file( :project_avatar, - File.join(Rails.root, 'public', 'gitlab_logo.png') + File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') ) click_button 'Save changes' @project.reload end step 'I should see new project avatar' do - @project.avatar.should be_instance_of AvatarUploader + expect(@project.avatar).to be_instance_of AvatarUploader url = @project.avatar.url - url.should == "/uploads/project/avatar/#{ @project.id }/gitlab_logo.png" + expect(url).to eq "/uploads/project/avatar/#{ @project.id }/banana_sample.gif" end step 'I should see the "Remove avatar" button' do - page.should have_link('Remove avatar') + expect(page).to have_link('Remove avatar') end step 'I have an project avatar' do attach_file( :project_avatar, - File.join(Rails.root, 'public', 'gitlab_logo.png') + File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') ) click_button 'Save changes' @project.reload @@ -59,16 +59,16 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see the default project avatar' do - @project.avatar?.should be_false + expect(@project.avatar?).to eq false end step 'I should not see the "Remove avatar" button' do - page.should_not have_link('Remove avatar') + expect(page).not_to have_link('Remove avatar') end step 'I should see project "Shop" version' do - within '.project-side' do - page.should have_content 'Version: 6.7.0.pre' + page.within '.project-side' do + expect(page).to have_content '6.7.0.pre' end end @@ -78,7 +78,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project default branch changed' do - find(:css, 'select#project_default_branch').value.should == 'fix' + expect(find(:css, 'select#project_default_branch').value).to eq 'fix' end step 'I select project "Forum" README tab' do @@ -86,12 +86,48 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project "Forum" README' do - page.should have_link 'README.md' - page.should have_content 'Sample repo for testing gitlab features' + page.within('#README') do + expect(page).to have_content 'Sample repo for testing gitlab features' + end end step 'I should see project "Shop" README' do - page.should have_link 'README.md' - page.should have_content 'testme' + page.within('#README') do + expect(page).to have_content 'testme' + end + end + + step 'I add project tags' do + fill_in 'Tags', with: 'tag1, tag2' + end + + step 'I should see project tags' do + expect(find_field('Tags').value).to eq 'tag1, tag2' + end + + step 'I should not see "New Issue" button' do + expect(page).not_to have_link 'New Issue' + end + + step 'I should not see "New Merge Request" button' do + expect(page).not_to have_link 'New Merge Request' + end + + step 'I should not see "Snippets" button' do + expect(page).not_to have_link 'Snippets' + end + + step 'project "Shop" belongs to group' do + group = create(:group) + @project.namespace = group + @project.save! + end + + step 'I should see back to dashboard button' do + expect(page).to have_content 'Back to Dashboard' + end + + step 'I should see back to group button' do + expect(page).to have_content 'Back to Group' end end diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb index a10e7bf78e..49e9c5520b 100644 --- a/features/steps/project/project_shortcuts.rb +++ b/features/steps/project/project_shortcuts.rb @@ -33,4 +33,9 @@ class Spinach::Features::ProjectShortcuts < Spinach::FeatureSteps find('body').native.send_key('g') find('body').native.send_key('w') end + + step 'I press "g" and "e"' do + find('body').native.send_key('g') + find('body').native.send_key('e') + end end diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index 57c6e39c80..0e724138a8 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -18,8 +18,8 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I should see project "Community" home page' do Gitlab.config.gitlab.should_receive(:host).and_return("www.example.com") - within '.navbar-gitlab .title' do - page.should have_content 'Community' + page.within '.navbar-gitlab .title' do + expect(page).to have_content 'Community' end end @@ -48,8 +48,8 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I should be redirected to "Community" page' do project = Project.find_by(name: 'Community') - current_path.should == "/#{project.path_with_namespace}" - status_code.should == 200 + expect(current_path).to eq "/#{project.path_with_namespace}" + expect(status_code).to eq 200 end step 'I get redirected to signin page where I sign in' do @@ -63,7 +63,7 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I should be redirected to "Enterprise" page' do project = Project.find_by(name: 'Enterprise') - current_path.should == "/#{project.path_with_namespace}" - status_code.should == 200 + expect(current_path).to eq "/#{project.path_with_namespace}" + expect(status_code).to eq 200 end end diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 4b3d79324a..0327fd6198 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -8,16 +8,16 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see list of available services' do - page.should have_content 'Project services' - page.should have_content 'Campfire' - page.should have_content 'HipChat' - page.should have_content 'GitLab CI' - page.should have_content 'Assembla' - page.should have_content 'Pushover' - page.should have_content 'Atlassian Bamboo' - page.should have_content 'JetBrains TeamCity' - page.should have_content 'Asana' - page.should have_content 'Irker (IRC gateway)' + expect(page).to have_content 'Project services' + expect(page).to have_content 'Campfire' + expect(page).to have_content 'HipChat' + expect(page).to have_content 'GitLab CI' + expect(page).to have_content 'Assembla' + expect(page).to have_content 'Pushover' + expect(page).to have_content 'Atlassian Bamboo' + expect(page).to have_content 'JetBrains TeamCity' + expect(page).to have_content 'Asana' + expect(page).to have_content 'Irker (IRC gateway)' end step 'I click gitlab-ci service link' do @@ -32,7 +32,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see service settings saved' do - find_field('Project url').value.should == 'http://ci.gitlab.org/projects/3' + expect(find_field('Project url').value).to eq 'http://ci.gitlab.org/projects/3' end step 'I click hipchat service link' do @@ -47,7 +47,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see hipchat service settings saved' do - find_field('Room').value.should == 'gitlab' + expect(find_field('Room').value).to eq 'gitlab' end step 'I fill hipchat settings with custom server' do @@ -59,7 +59,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see hipchat service settings with custom server saved' do - find_field('Server').value.should == 'https://chat.example.com' + expect(find_field('Server').value).to eq 'https://chat.example.com' end step 'I click pivotaltracker service link' do @@ -73,7 +73,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see pivotaltracker service settings saved' do - find_field('Token').value.should == 'verySecret' + expect(find_field('Token').value).to eq 'verySecret' end step 'I click Flowdock service link' do @@ -87,7 +87,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Flowdock service settings saved' do - find_field('Token').value.should == 'verySecret' + expect(find_field('Token').value).to eq 'verySecret' end step 'I click Assembla service link' do @@ -101,7 +101,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Assembla service settings saved' do - find_field('Token').value.should == 'verySecret' + expect(find_field('Token').value).to eq 'verySecret' end step 'I click Asana service link' do @@ -116,8 +116,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Asana service settings saved' do - find_field('Api key').value.should == 'verySecret' - find_field('Restrict to branch').value.should == 'master' + expect(find_field('Api key').value).to eq 'verySecret' + expect(find_field('Restrict to branch').value).to eq 'master' end step 'I click email on push service link' do @@ -130,7 +130,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see email on push service settings saved' do - find_field('Recipients').value.should == 'qa@company.name' + expect(find_field('Recipients').value).to eq 'qa@company.name' end step 'I click Irker service link' do @@ -145,8 +145,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Irker service settings saved' do - find_field('Recipients').value.should == 'irc://chat.freenode.net/#commits' - find_field('Colorize messages').value.should == '1' + expect(find_field('Recipients').value).to eq 'irc://chat.freenode.net/#commits' + expect(find_field('Colorize messages').value).to eq '1' end step 'I click Slack service link' do @@ -160,7 +160,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Slack service settings saved' do - find_field('Webhook').value.should == 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' + expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' end step 'I click Pushover service link' do @@ -178,11 +178,11 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Pushover service settings saved' do - find_field('Api key').value.should == 'verySecret' - find_field('User key').value.should == 'verySecret' - find_field('Device').value.should == 'myDevice' - find_field('Priority').find('option[selected]').value.should == '1' - find_field('Sound').find('option[selected]').value.should == 'bike' + expect(find_field('Api key').value).to eq 'verySecret' + expect(find_field('User key').value).to eq 'verySecret' + expect(find_field('Device').value).to eq 'myDevice' + expect(find_field('Priority').find('option[selected]').value).to eq '1' + expect(find_field('Sound').find('option[selected]').value).to eq 'bike' end step 'I click Atlassian Bamboo CI service link' do @@ -199,9 +199,9 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see Atlassian Bamboo CI service settings saved' do - find_field('Bamboo url').value.should == 'http://bamboo.example.com' - find_field('Build key').value.should == 'KEY' - find_field('Username').value.should == 'user' + expect(find_field('Bamboo url').value).to eq 'http://bamboo.example.com' + expect(find_field('Build key').value).to eq 'KEY' + expect(find_field('Username').value).to eq 'user' end step 'I click JetBrains TeamCity CI service link' do @@ -218,8 +218,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see JetBrains TeamCity CI service settings saved' do - find_field('Teamcity url').value.should == 'http://teamcity.example.com' - find_field('Build type').value.should == 'GitlabTest_Build' - find_field('Username').value.should == 'user' + expect(find_field('Teamcity url').value).to eq 'http://teamcity.example.com' + expect(find_field('Build type').value).to eq 'GitlabTest_Build' + expect(find_field('Username').value).to eq 'user' end end diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 343aeb53b1..db8ad08bb9 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -30,19 +30,19 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I should see "Snippet one" in snippets' do - page.should have_content "Snippet one" + expect(page).to have_content "Snippet one" end step 'I should not see "Snippet two" in snippets' do - page.should_not have_content "Snippet two" + expect(page).not_to have_content "Snippet two" end step 'I should not see "Snippet one" in snippets' do - page.should_not have_content "Snippet one" + expect(page).not_to have_content "Snippet one" end step 'I click link "Edit"' do - within ".file-title" do + page.within ".file-title" do click_link "Edit" end end @@ -52,37 +52,37 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I submit new snippet "Snippet three"' do - fill_in "project_snippet_title", :with => "Snippet three" - fill_in "project_snippet_file_name", :with => "my_snippet.rb" - within('.file-editor') do + fill_in "project_snippet_title", with: "Snippet three" + fill_in "project_snippet_file_name", with: "my_snippet.rb" + page.within('.file-editor') do find(:xpath, "//input[@id='project_snippet_content']").set 'Content of snippet three' end click_button "Create snippet" end step 'I should see snippet "Snippet three"' do - page.should have_content "Snippet three" - page.should have_content "Content of snippet three" + expect(page).to have_content "Snippet three" + expect(page).to have_content "Content of snippet three" end step 'I submit new title "Snippet new title"' do - fill_in "project_snippet_title", :with => "Snippet new title" + fill_in "project_snippet_title", with: "Snippet new title" click_button "Save" end step 'I should see "Snippet new title"' do - page.should have_content "Snippet new title" + expect(page).to have_content "Snippet new title" end step 'I leave a comment like "Good snippet!"' do - within('.js-main-target-form') do + page.within('.js-main-target-form') do fill_in "note_note", with: "Good snippet!" click_button "Add Comment" end end step 'I should see comment "Good snippet!"' do - page.should have_content "Good snippet!" + expect(page).to have_content "Good snippet!" end step 'I visit snippet page "Snippet one"' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index caf6c73ee0..5cb085db20 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -5,23 +5,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps include RepoHelpers step 'I should see files from repository' do - page.should have_content "VERSION" - page.should have_content ".gitignore" - page.should have_content "LICENSE" + expect(page).to have_content "VERSION" + expect(page).to have_content ".gitignore" + expect(page).to have_content "LICENSE" end step 'I should see files from repository for "6d39438"' do - current_path.should == namespace_project_tree_path(@project.namespace, @project, "6d39438") - page.should have_content ".gitignore" - page.should have_content "LICENSE" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "6d39438") + expect(page).to have_content ".gitignore" + expect(page).to have_content "LICENSE" end step 'I see the ".gitignore"' do - page.should have_content '.gitignore' + expect(page).to have_content '.gitignore' end step 'I don\'t see the ".gitignore"' do - page.should_not have_content '.gitignore' + expect(page).not_to have_content '.gitignore' end step 'I click on ".gitignore" file in repo' do @@ -29,11 +29,11 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see its content' do - page.should have_content old_gitignore_content + expect(page).to have_content old_gitignore_content end step 'I should see its new content' do - page.should have_content new_gitignore_content + expect(page).to have_content new_gitignore_content end step 'I click link "Raw"' do @@ -41,7 +41,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see raw file content' do - source.should == sample_blob.data + expect(source).to eq sample_blob.data end step 'I click button "Edit"' do @@ -49,16 +49,16 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I cannot see the edit button' do - page.should_not have_link 'edit' + expect(page).not_to have_link 'edit' end step 'The edit button is disabled' do - page.should have_css '.disabled', text: 'Edit' + expect(page).to have_css '.disabled', text: 'Edit' end step 'I can edit code' do set_new_content - evaluate_script('blob.editor.getValue()').should == new_gitignore_content + expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content end step 'I edit code' do @@ -98,7 +98,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see diff' do - page.should have_css '.line_holder.new' + expect(page).to have_css '.line_holder.new' end step 'I click on "new file" link in repo' do @@ -106,8 +106,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I can see new file page' do - page.should have_content "New file" - page.should have_content "Commit message" + expect(page).to have_content "New file" + expect(page).to have_content "Commit message" end step 'I click on files directory' do @@ -119,25 +119,25 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see Browse dir link' do - page.should have_link 'Browse Dir »' - page.should_not have_link 'Browse Code »' + expect(page).to have_link 'Browse Dir »' + expect(page).not_to have_link 'Browse Code »' end step 'I click on readme file' do - within '.tree-table' do + page.within '.tree-table' do click_link 'README.md' end end step 'I see Browse file link' do - page.should have_link 'Browse File »' - page.should_not have_link 'Browse Code »' + expect(page).to have_link 'Browse File »' + expect(page).not_to have_link 'Browse Code »' end step 'I see Browse code link' do - page.should have_link 'Browse Code »' - page.should_not have_link 'Browse File »' - page.should_not have_link 'Browse Dir »' + expect(page).to have_link 'Browse Code »' + expect(page).not_to have_link 'Browse File »' + expect(page).not_to have_link 'Browse Dir »' end step 'I click on Permalink' do @@ -145,7 +145,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I am redirected to the files URL' do - current_path.should == namespace_project_tree_path(@project.namespace, @project, 'master') + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, 'master') end step 'I am redirected to the ".gitignore"' do @@ -187,12 +187,29 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I click on "add a file" link' do - click_link 'add a file' + click_link 'adding README' # Remove pre-receive hook so we can push without auth FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive')) end + step "I switch ref to 'test'" do + select "'test'", from: 'ref' + end + + step "I see the ref 'test' has been selected" do + expect(page).to have_selector '.select2-chosen', text: "'test'" + end + + step "I visit the 'test' tree" do + visit namespace_project_tree_path(@project.namespace, @project, "'test'") + end + + step 'I see the commit data' do + expect(page).to have_css('.tree-commit-link', visible: true) + expect(page).not_to have_content('Loading commit data...') + end + private def set_new_content diff --git a/features/steps/project/source/git_blame.rb b/features/steps/project/source/git_blame.rb index e29a816c51..d0a27f47e2 100644 --- a/features/steps/project/source/git_blame.rb +++ b/features/steps/project/source/git_blame.rb @@ -12,8 +12,8 @@ class Spinach::Features::ProjectSourceGitBlame < Spinach::FeatureSteps end step 'I should see git file blame' do - page.should have_content "*.rb" - page.should have_content "Dmitriy Zaporozhets" - page.should have_content "Initial commit" + expect(page).to have_content "*.rb" + expect(page).to have_content "Dmitriy Zaporozhets" + expect(page).to have_content "Initial commit" end end diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index 7961fdedad..c78e86fa1a 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -13,19 +13,19 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see files from repository in markdown' do - current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown") - page.should have_content "README.md" - page.should have_content "CHANGELOG" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown") + expect(page).to have_content "README.md" + expect(page).to have_content "CHANGELOG" end step 'I should see rendered README which contains correct links' do - page.should have_content "Welcome to GitLab GitLab is a free project and repository management application" - page.should have_link "GitLab API doc" - page.should have_link "GitLab API website" - page.should have_link "Rake tasks" - page.should have_link "backup and restore procedure" - page.should have_link "GitLab API doc directory" - page.should have_link "Maintenance" + expect(page).to have_content "Welcome to GitLab GitLab is a free project and repository management application" + expect(page).to have_link "GitLab API doc" + expect(page).to have_link "GitLab API website" + expect(page).to have_link "Rake tasks" + expect(page).to have_link "backup and restore procedure" + expect(page).to have_link "GitLab API doc directory" + expect(page).to have_link "Maintenance" end step 'I click on Gitlab API in README' do @@ -33,8 +33,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct document rendered' do - current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") - page.should have_content "All API requests require authentication" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") + expect(page).to have_content "All API requests require authentication" end step 'I click on Rake tasks in README' do @@ -42,9 +42,9 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct directory rendered' do - current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown/doc/raketasks") - page.should have_content "backup_restore.md" - page.should have_content "maintenance.md" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown/doc/raketasks") + expect(page).to have_content "backup_restore.md" + expect(page).to have_content "maintenance.md" end step 'I click on GitLab API doc directory in README' do @@ -52,9 +52,9 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct doc/api directory rendered' do - current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") - page.should have_content "README.md" - page.should have_content "users.md" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") + expect(page).to have_content "README.md" + expect(page).to have_content "users.md" end step 'I click on Maintenance in README' do @@ -62,41 +62,41 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct maintenance file rendered' do - current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/raketasks/maintenance.md") - page.should have_content "bundle exec rake gitlab:env:info RAILS_ENV=production" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/raketasks/maintenance.md") + expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production" end step 'I click on link "empty" in the README' do - within('.readme-holder') do + page.within('.readme-holder') do click_link "empty" end end step 'I click on link "id" in the README' do - within('.readme-holder') do + page.within('.readme-holder') do click_link "#id" end end step 'I navigate to the doc/api/README' do - within '.tree-table' do + page.within '.tree-table' do click_link "doc" end - within '.tree-table' do + page.within '.tree-table' do click_link "api" end - within '.tree-table' do + page.within '.tree-table' do click_link "README.md" end end step 'I see correct file rendered' do - current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") - page.should have_content "Contents" - page.should have_link "Users" - page.should have_link "Rake tasks" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") + expect(page).to have_content "Contents" + expect(page).to have_link "Users" + expect(page).to have_link "Rake tasks" end step 'I click on users in doc/api/README' do @@ -104,8 +104,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see the correct document file' do - current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") - page.should have_content "Get a list of users." + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") + expect(page).to have_content "Get a list of users." end step 'I click on raketasks in doc/api/README' do @@ -131,32 +131,32 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see files from repository in markdown branch' do - current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown") - page.should have_content "README.md" - page.should have_content "CHANGELOG" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown") + expect(page).to have_content "README.md" + expect(page).to have_content "CHANGELOG" end step 'I see correct file rendered in markdown branch' do - current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") - page.should have_content "Contents" - page.should have_link "Users" - page.should have_link "Rake tasks" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") + expect(page).to have_content "Contents" + expect(page).to have_link "Users" + expect(page).to have_link "Rake tasks" end step 'I should see correct document rendered for markdown branch' do - current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") - page.should have_content "All API requests require authentication" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") + expect(page).to have_content "All API requests require authentication" end step 'I should see correct directory rendered for markdown branch' do - current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown/doc/raketasks") - page.should have_content "backup_restore.md" - page.should have_content "maintenance.md" + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown/doc/raketasks") + expect(page).to have_content "backup_restore.md" + expect(page).to have_content "maintenance.md" end step 'I should see the users document file in markdown branch' do - current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") - page.should have_content "Get a list of users." + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") + expect(page).to have_content "Get a list of users." end # Expected link contents @@ -208,7 +208,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I go to wiki page' do click_link "Wiki" - current_path.should == namespace_project_wiki_path(@project.namespace, @project, "home") + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "home") end step 'I add various links to the wiki page' do @@ -218,8 +218,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'Wiki page should have added links' do - current_path.should == namespace_project_wiki_path(@project.namespace, @project, "home") - page.should have_content "test GitLab API doc Rake tasks" + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "home") + expect(page).to have_content "test GitLab API doc Rake tasks" end step 'I add a header to the wiki page' do @@ -237,13 +237,13 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see new wiki page named test' do - current_path.should == namespace_project_wiki_path(@project.namespace, @project, "test") - page.should have_content "Editing" + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "test") + expect(page).to have_content "Editing" end When 'I go back to wiki page home' do visit namespace_project_wiki_path(@project.namespace, @project, "home") - current_path.should == namespace_project_wiki_path(@project.namespace, @project, "home") + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "home") end step 'I click on GitLab API doc link' do @@ -251,8 +251,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see Gitlab API document' do - current_path.should == namespace_project_wiki_path(@project.namespace, @project, "api") - page.should have_content "Editing" + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api") + expect(page).to have_content "Editing" end step 'I click on Rake tasks link' do @@ -260,13 +260,13 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see Rake tasks directory' do - current_path.should == namespace_project_wiki_path(@project.namespace, @project, "raketasks") - page.should have_content "Editing" + expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks") + expect(page).to have_content "Editing" end step 'I go directory which contains README file' do visit namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") - current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") + expect(current_path).to eq namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") end step 'I click on a relative link in README' do @@ -274,8 +274,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see the correct markdown' do - current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") - page.should have_content "List users" + expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") + expect(page).to have_content "List users" end step 'Header "Application details" should have correct id and link' do diff --git a/features/steps/project/source/multiselect_blob.rb b/features/steps/project/source/multiselect_blob.rb deleted file mode 100644 index b749ba4937..0000000000 --- a/features/steps/project/source/multiselect_blob.rb +++ /dev/null @@ -1,58 +0,0 @@ -class Spinach::Features::ProjectSourceMultiselectBlob < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - class << self - def click_line_steps(*line_numbers) - line_numbers.each do |line_number| - step "I click line #{line_number} in file" do - find("#L#{line_number}").click - end - - step "I shift-click line #{line_number} in file" do - script = "$('#L#{line_number}').trigger($.Event('click', { shiftKey: true }));" - execute_script(script) - end - end - end - - def check_state_steps(*ranges) - ranges.each do |range| - fragment = range.kind_of?(Array) ? "L#{range.first}-#{range.last}" : "L#{range}" - pluralization = range.kind_of?(Array) ? "s" : "" - - step "I should see \"#{fragment}\" as URI fragment" do - URI.parse(current_url).fragment.should == fragment - end - - step "I should see line#{pluralization} #{fragment[1..-1]} highlighted" do - ids = Array(range).map { |n| "LC#{n}" } - extra = false - - highlighted = all("#tree-content-holder .highlight .line.hll") - highlighted.each do |element| - extra ||= ids.delete(element[:id]).nil? - end - - extra.should be_false and ids.should be_empty - end - end - end - end - - click_line_steps *Array(1..5) - check_state_steps *Array(1..5), Array(1..2), Array(1..3), Array(1..4), Array(1..5), Array(3..5) - - step 'I go back in history' do - go_back - end - - step 'I go forward in history' do - go_forward - end - - step 'I click on ".gitignore" file in repo' do - click_link ".gitignore" - end -end diff --git a/features/steps/project/source/search_code.rb b/features/steps/project/source/search_code.rb index 9c2864cc93..feee756d7e 100644 --- a/features/steps/project/source/search_code.rb +++ b/features/steps/project/source/search_code.rb @@ -9,11 +9,11 @@ class Spinach::Features::ProjectSourceSearchCode < Spinach::FeatureSteps end step 'I should see files from repository containing "coffee"' do - page.should have_content 'coffee' - page.should have_content 'CONTRIBUTING.md' + expect(page).to have_content 'coffee' + expect(page).to have_content 'CONTRIBUTING.md' end step 'I should see empty result' do - page.should have_content "We couldn't find any matching" + expect(page).to have_content "We couldn't find any" end end diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb index 50cdfd73c3..bd2e0619cd 100644 --- a/features/steps/project/star.rb +++ b/features/steps/project/star.rb @@ -5,7 +5,7 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps include SharedUser step "The project has no stars" do - page.should_not have_content '.star-buttons' + expect(page).not_to have_content '.toggle-star' end step "The project has 0 stars" do @@ -26,7 +26,7 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps end step 'I redirected to sign in page' do - current_path.should == new_user_session_path + expect(current_path).to eq new_user_session_path end protected diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index e95621071c..97d6301645 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -5,14 +5,14 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps include Select2Helper step 'I should be able to see myself in team' do - page.should have_content(@user.name) - page.should have_content(@user.username) + expect(page).to have_content(@user.name) + expect(page).to have_content(@user.username) end - step 'I should see "Sam" in team list' do - user = User.find_by(name: "Sam") - page.should have_content(user.name) - page.should have_content(user.username) + step 'I should see "Dmitriy" in team list' do + user = User.find_by(name: "Dmitriy") + expect(page).to have_content(user.name) + expect(page).to have_content(user.username) end step 'I click link "Add members"' do @@ -22,7 +22,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I select "Mike" as "Reporter"' do user = User.find_by(name: "Mike") - within ".users-project-form" do + page.within ".users-project-form" do select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end @@ -30,13 +30,13 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps end step 'I should see "Mike" in team list as "Reporter"' do - within ".access-reporter" do - page.should have_content('Mike') + page.within ".access-reporter" do + expect(page).to have_content('Mike') end end step 'I select "sjobs@apple.com" as "Reporter"' do - within ".users-project-form" do + page.within ".users-project-form" do select2("sjobs@apple.com", from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end @@ -44,33 +44,33 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps end step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do - within ".access-reporter" do - page.should have_content('sjobs@apple.com') - page.should have_content('invited') - page.should have_content('Reporter') + page.within ".access-reporter" do + expect(page).to have_content('sjobs@apple.com') + expect(page).to have_content('invited') + expect(page).to have_content('Reporter') end end - step 'I should see "Sam" in team list as "Developer"' do - within ".access-developer" do - page.should have_content('Sam') + step 'I should see "Dmitriy" in team list as "Developer"' do + page.within ".access-developer" do + expect(page).to have_content('Dmitriy') end end - step 'I change "Sam" role to "Reporter"' do + step 'I change "Dmitriy" role to "Reporter"' do project = Project.find_by(name: "Shop") - user = User.find_by(name: 'Sam') + user = User.find_by(name: 'Dmitriy') project_member = project.project_members.find_by(user_id: user.id) - within "#project_member_#{project_member.id}" do + page.within "#project_member_#{project_member.id}" do click_button "Edit access level" select "Reporter", from: "project_member_access_level" click_button "Save" end end - step 'I should see "Sam" in team list as "Reporter"' do - within ".access-reporter" do - page.should have_content('Sam') + step 'I should see "Dmitriy" in team list as "Reporter"' do + page.within ".access-reporter" do + expect(page).to have_content('Dmitriy') end end @@ -78,22 +78,22 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps click_link "Remove from team" end - step 'I should not see "Sam" in team list' do - user = User.find_by(name: "Sam") - page.should_not have_content(user.name) - page.should_not have_content(user.username) + step 'I should not see "Dmitriy" in team list' do + user = User.find_by(name: "Dmitriy") + expect(page).not_to have_content(user.name) + expect(page).not_to have_content(user.username) end step 'gitlab user "Mike"' do create(:user, name: "Mike") end - step 'gitlab user "Sam"' do - create(:user, name: "Sam") + step 'gitlab user "Dmitriy"' do + create(:user, name: "Dmitriy") end - step '"Sam" is "Shop" developer' do - user = User.find_by(name: "Sam") + step '"Dmitriy" is "Shop" developer' do + user = User.find_by(name: "Dmitriy") project = Project.find_by(name: "Shop") project.team << [user, :developer] end @@ -119,11 +119,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps click_button 'Import' end - step 'I click cancel link for "Sam"' do + step 'I click cancel link for "Dmitriy"' do project = Project.find_by(name: "Shop") - user = User.find_by(name: 'Sam') + user = User.find_by(name: 'Dmitriy') project_member = project.project_members.find_by(user_id: user.id) - within "#project_member_#{project_member.id}" do + page.within "#project_member_#{project_member.id}" do click_link('Remove user from team') end end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index bb93e582a1..eebfaee1ed 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -6,13 +6,13 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps include WikiHelper step 'I click on the Cancel button' do - within(:css, ".form-actions") do + page.within(:css, ".form-actions") do click_on "Cancel" end end step 'I should be redirected back to the Edit Home Wiki page' do - current_path.should == namespace_project_wiki_path(project.namespace, project, :home) + expect(current_path).to eq namespace_project_wiki_path(project.namespace, project, :home) end step 'I create the Wiki Home page' do @@ -21,11 +21,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the newly created wiki page' do - page.should have_content "Home" - page.should have_content "link test" + expect(page).to have_content "Home" + expect(page).to have_content "link test" click_link "link test" - page.should have_content "Editing" + expect(page).to have_content "Editing" end step 'I have an existing Wiki page' do @@ -47,11 +47,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the updated content' do - page.should have_content "Updated Wiki Content" + expect(page).to have_content "Updated Wiki Content" end step 'I should be redirected back to that Wiki page' do - current_path.should == namespace_project_wiki_path(project.namespace, project, @page) + expect(current_path).to eq namespace_project_wiki_path(project.namespace, project, @page) end step 'That page has two revisions' do @@ -63,9 +63,9 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see both revisions' do - page.should have_content current_user.name - page.should have_content "first commit" - page.should have_content "second commit" + expect(page).to have_content current_user.name + expect(page).to have_content "first commit" + expect(page).to have_content "second commit" end step 'I click on the "Delete this page" button' do @@ -73,7 +73,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'The page should be deleted' do - page.should have_content "Page was successfully deleted" + expect(page).to have_content "Page was successfully deleted" end step 'I click on the "Pages" button' do @@ -81,8 +81,8 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the existing page in the pages list' do - page.should have_content current_user.name - page.should have_content @page.title + expect(page).to have_content current_user.name + expect(page).to have_content @page.title end step 'I have an existing Wiki page with images linked on page' do @@ -98,30 +98,30 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps file = Gollum::File.new(wiki.wiki) Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file) Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg") - page.should have_link('image', href: "image.jpg") + expect(page).to have_link('image', href: "image.jpg") click_on "image" end step 'I should see the image from wiki repo' do - current_path.should match('wikis/image.jpg') - page.should_not have_xpath('/html') # Page should render the image which means there is no html involved + expect(current_path).to match('wikis/image.jpg') + expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved Gollum::Wiki.any_instance.unstub(:file) Gollum::File.any_instance.unstub(:mime_type) end step 'Image should be shown on the page' do - page.should have_xpath("//img[@src=\"image.jpg\"]") + expect(page).to have_xpath("//img[@src=\"image.jpg\"]") end step 'I click on image link' do - page.should have_link('image', href: "image.jpg") + expect(page).to have_link('image', href: "image.jpg") click_on "image" end step 'I should see the new wiki page form' do - current_path.should match('wikis/image.jpg') - page.should have_content('New Wiki Page') - page.should have_content('Editing - image.jpg') + expect(current_path).to match('wikis/image.jpg') + expect(page).to have_content('New Wiki Page') + expect(page).to have_content('Editing - image.jpg') end step 'I create a New page with paths' do @@ -130,11 +130,21 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps click_on 'Build' fill_in "wiki_content", with: 'wiki content' click_on "Create page" - current_path.should include 'one/two/three' + expect(current_path).to include 'one/two/three' + end + + step 'I create a New page with an invalid name' do + click_on 'New Page' + fill_in 'Page slug', with: 'invalid name' + click_on 'Build' + end + + step 'I should see an error message' do + expect(page).to have_content "The page slug is invalid" end step 'I should see non-escaped link in the pages list' do - page.should have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']") + expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']") end step 'I edit the Wiki page with a path' do @@ -143,11 +153,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see a non-escaped path' do - current_path.should include 'one/two/three' + expect(current_path).to include 'one/two/three' end step 'I should see the Editing page' do - page.should have_content('Editing') + expect(page).to have_content('Editing') end step 'I view the page history of a Wiki page that has a path' do @@ -156,7 +166,12 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should see the page history' do - page.should have_content('History for') + expect(page).to have_content('History for') + end + + step 'I search for Wiki content' do + fill_in "Search in this project", with: "wiki_content" + click_button "Search" end def wiki diff --git a/features/steps/search.rb b/features/steps/search.rb index 6f0e038c4d..87893aa020 100644 --- a/features/steps/search.rb +++ b/features/steps/search.rb @@ -18,30 +18,43 @@ class Spinach::Features::Search < Spinach::FeatureSteps click_button "Search" end + step 'I search for "Wiki content"' do + fill_in "dashboard_search", with: "content" + click_button "Search" + end + step 'I click "Issues" link' do - within '.search-filter' do + page.within '.search-filter' do click_link 'Issues' end end step 'I click project "Shop" link' do - within '.project-filter' do + page.within '.project-filter' do click_link project.name_with_namespace end end step 'I click "Merge requests" link' do - within '.search-filter' do + page.within '.search-filter' do click_link 'Merge requests' end end + step 'I click "Wiki" link' do + page.within '.search-filter' do + click_link 'Wiki' + end + end + step 'I should see "Shop" project link' do - page.should have_link "Shop" + expect(page).to have_link "Shop" end step 'I should see code results for project "Shop"' do - page.should have_content 'Update capybara, rspec-rails, poltergeist to recent versions' + page.within('.results') do + page.should have_content 'Update capybara, rspec-rails, poltergeist to recent versions' + end end step 'I search for "Contibuting"' do @@ -60,10 +73,23 @@ class Spinach::Features::Search < Spinach::FeatureSteps end step 'I should see "Foo" link in the search results' do - find(:css, '.search-results').should have_link 'Foo' + page.within('.results') do + find(:css, '.search-results').should have_link 'Foo' + end end step 'I should not see "Bar" link in the search results' do - find(:css, '.search-results').should_not have_link 'Bar' + expect(find(:css, '.search-results')).not_to have_link 'Bar' + end + + step 'I should see "test_wiki" link in the search results' do + page.within('.results') do + find(:css, '.search-results').should have_link 'test_wiki.md' + end + end + + step 'project has Wiki content' do + @wiki = ::ProjectWiki.new(project, current_user) + @wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit") end end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index 9beb688bd1..72d873caa5 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -2,27 +2,27 @@ module SharedActiveTab include Spinach::DSL def ensure_active_main_tab(content) - find('.nav-sidebar > li.active').should have_content(content) + expect(find('.nav-sidebar > li.active')).to have_content(content) end def ensure_active_sub_tab(content) - find('div.content ul.nav-tabs li.active').should have_content(content) + expect(find('div.content ul.nav-tabs li.active')).to have_content(content) end def ensure_active_sub_nav(content) - find('.sidebar-subnav > li.active').should have_content(content) + expect(find('.sidebar-subnav > li.active')).to have_content(content) end step 'no other main tabs should be active' do - page.should have_selector('.nav-sidebar > li.active', count: 1) + expect(page).to have_selector('.nav-sidebar > li.active', count: 1) end step 'no other sub tabs should be active' do - page.should have_selector('div.content ul.nav-tabs li.active', count: 1) + expect(page).to have_selector('div.content ul.nav-tabs li.active', count: 1) end step 'no other sub navs should be active' do - page.should have_selector('.sidebar-subnav > li.active', count: 1) + expect(page).to have_selector('.sidebar-subnav > li.active', count: 1) end step 'the active main tab should be Home' do diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb index b607299567..fbaa408226 100644 --- a/features/steps/shared/admin.rb +++ b/features/steps/shared/admin.rb @@ -9,4 +9,3 @@ module SharedAdmin 2.times { create(:user) } end end - diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index ac8a3df6bb..735e0ef610 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -21,13 +21,17 @@ module SharedAuthentication end step 'I should be redirected to sign in page' do - current_path.should == new_user_session_path + expect(current_path).to eq new_user_session_path end step "I logout" do logout end + step "I logout directly" do + logout_direct + end + def current_user @user || User.first end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 510e0f0f93..27a95aeb19 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -3,7 +3,7 @@ module SharedDiffNote include RepoHelpers step 'I cancel the diff comment' do - within(diff_file_selector) do + page.within(diff_file_selector) do find(".js-close-discussion-note-form").click end end @@ -14,154 +14,206 @@ module SharedDiffNote end step 'I haven\'t written any diff comment text' do - within(diff_file_selector) do + page.within(diff_file_selector) do fill_in "note[note]", with: "" end end step 'I leave a diff comment like "Typo, please fix"' do - click_diff_line(sample_commit.line_code) - within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "Typo, please fix" + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[rel$='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "Typo, please fix" + find(".js-comment-button").trigger("click") + sleep 0.05 + end + end + end + + step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do + click_parallel_diff_line(sample_commit.line_code, 'old') + page.within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "Old comment" + find(".js-comment-button").trigger("click") + end + end + + step 'I leave a diff comment in a parallel view on the right side like "New comment"' do + click_parallel_diff_line(sample_commit.line_code, 'new') + page.within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") - sleep 0.05 end end step 'I preview a diff comment text like "Should fix it :smile:"' do - click_diff_line(sample_commit.line_code) - within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "Should fix it :smile:" - find('.js-md-preview-button').click + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[rel$='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "Should fix it :smile:" + find('.js-md-preview-button').click + end end end step 'I preview another diff comment text like "DRY this up"' do - click_diff_line(sample_commit.del_line_code) + page.within(diff_file_selector) do + click_diff_line(sample_commit.del_line_code) - within("#{diff_file_selector} form[rel$='#{sample_commit.del_line_code}']") do - fill_in "note[note]", with: "DRY this up" - find('.js-md-preview-button').click + page.within("form[rel$='#{sample_commit.del_line_code}']") do + fill_in "note[note]", with: "DRY this up" + find('.js-md-preview-button').click + end end end step 'I open a diff comment form' do - click_diff_line(sample_commit.line_code) + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + end end step 'I open another diff comment form' do - click_diff_line(sample_commit.del_line_code) + page.within(diff_file_selector) do + click_diff_line(sample_commit.del_line_code) + end end step 'I write a diff comment like ":-1: I don\'t like this"' do - within(diff_file_selector) do + page.within(diff_file_selector) do fill_in "note[note]", with: ":-1: I don\'t like this" end end step 'I submit the diff comment' do - within(diff_file_selector) do + page.within(diff_file_selector) do click_button("Add Comment") end end step 'I should not see the diff comment form' do - within(diff_file_selector) do - page.should_not have_css("form.new_note") + page.within(diff_file_selector) do + expect(page).not_to have_css("form.new_note") end end step 'The diff comment preview tab should say there is nothing to do' do - within(diff_file_selector) do + page.within(diff_file_selector) do find('.js-md-preview-button').click expect(find('.js-md-preview')).to have_content('Nothing to preview.') end end step 'I should not see the diff comment text field' do - within(diff_file_selector) do + page.within(diff_file_selector) do expect(find('.js-note-text')).not_to be_visible end end step 'I should only see one diff form' do - within(diff_file_selector) do - page.should have_css("form.new_note", count: 1) + page.within(diff_file_selector) do + expect(page).to have_css("form.new_note", count: 1) end end step 'I should see a diff comment form with ":-1: I don\'t like this"' do - within(diff_file_selector) do - page.should have_field("note[note]", with: ":-1: I don\'t like this") + page.within(diff_file_selector) do + expect(page).to have_field("note[note]", with: ":-1: I don\'t like this") end end step 'I should see a diff comment saying "Typo, please fix"' do - within("#{diff_file_selector} .note") do - page.should have_content("Typo, please fix") + page.within("#{diff_file_selector} .note") do + expect(page).to have_content("Typo, please fix") + end + end + + step 'I should see a diff comment on the left side saying "Old comment"' do + page.within("#{diff_file_selector} .notes_content.parallel.old") do + expect(page).to have_content("Old comment") + end + end + + step 'I should see a diff comment on the right side saying "New comment"' do + page.within("#{diff_file_selector} .notes_content.parallel.new") do + expect(page).to have_content("New comment") end end step 'I should see a discussion reply button' do - within(diff_file_selector) do - page.should have_button('Reply') + page.within(diff_file_selector) do + expect(page).to have_button('Reply') end end step 'I should see a temporary diff comment form' do - within(diff_file_selector) do - page.should have_css(".js-temp-notes-holder form.new_note") + page.within(diff_file_selector) do + expect(page).to have_css(".js-temp-notes-holder form.new_note") end end step 'I should see add a diff comment button' do - page.should have_css('.js-add-diff-note-button', visible: true) + expect(page).to have_css('.js-add-diff-note-button', visible: true) end step 'I should see an empty diff comment form' do - within(diff_file_selector) do - page.should have_field("note[note]", with: "") + page.within(diff_file_selector) do + expect(page).to have_field("note[note]", with: "") end end step 'I should see the cancel comment button' do - within("#{diff_file_selector} form") do - page.should have_css(".js-close-discussion-note-form", text: "Cancel") + page.within("#{diff_file_selector} form") do + expect(page).to have_css(".js-close-discussion-note-form", text: "Cancel") end end step 'I should see the diff comment preview' do - within("#{diff_file_selector} form") do + page.within("#{diff_file_selector} form") do expect(page).to have_css('.js-md-preview', visible: true) end end step 'I should see the diff comment write tab' do - within(diff_file_selector) do + page.within(diff_file_selector) do expect(page).to have_css('.js-md-write-button', visible: true) end end step 'The diff comment preview tab should display rendered Markdown' do - within(diff_file_selector) do + page.within(diff_file_selector) do find('.js-md-preview-button').click expect(find('.js-md-preview')).to have_css('img.emoji', visible: true) end end step 'I should see two separate previews' do - within(diff_file_selector) do + page.within(diff_file_selector) do expect(page).to have_css('.js-md-preview', visible: true, count: 2) expect(page).to have_content('Should fix it') expect(page).to have_content('DRY this up') end end + step 'I click side-by-side diff button' do + click_link "Side-by-side" + end + + step 'I see side-by-side diff button' do + expect(page).to have_content "Side-by-side" + end + def diff_file_selector - ".diff-file:nth-of-type(1)" + '.diff-file:nth-of-type(1)' end def click_diff_line(code) find("button[data-line-code='#{code}']").click end + + def click_parallel_diff_line(code, line_type) + find("button[data-line-code='#{code}'][data-line-type='#{line_type}']").trigger('click') + end end diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb index 1b225dd61a..2d17fb34cc 100644 --- a/features/steps/shared/group.rb +++ b/features/steps/shared/group.rb @@ -22,11 +22,11 @@ module SharedGroup end step 'I should see group "TestGroup"' do - page.should have_content "TestGroup" + expect(page).to have_content "TestGroup" end step 'I should not see group "TestGroup"' do - page.should_not have_content "TestGroup" + expect(page).not_to have_content "TestGroup" end protected diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index 41db2612f2..e6d1b8b8ef 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -2,7 +2,7 @@ module SharedIssuable include Spinach::DSL def edit_issuable - find(:css, '.issuable-edit').click + find(:css, '.issuable-edit').click end step 'I click link "Edit" for the merge request' do diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb index e71700880c..56b36f7c46 100644 --- a/features/steps/shared/markdown.rb +++ b/features/steps/shared/markdown.rb @@ -2,65 +2,24 @@ module SharedMarkdown include Spinach::DSL def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki") - find(:css, "#{parent} h#{level}##{id}").text.should == text - find(:css, "#{parent} h#{level}##{id} > :last-child")[:href].should =~ /##{id}$/ - end + node = find("#{parent} h#{level} a##{id}") + expect(node[:href]).to eq "##{id}" - def create_taskable(type, title) - desc_text = < .note-text") do - page.should have_content("Comment with a header") - page.should_not have_css("#comment-with-a-header") + page.within(".note-body > .note-text") do + expect(page).to have_content("Comment with a header") + expect(page).not_to have_css("#comment-with-a-header") end end - step 'I leave a comment with task markdown' do - within('.js-main-target-form') do - fill_in 'note[note]', with: '* [x] Task item' - click_button 'Add Comment' - sleep 0.05 - end - end - - step 'I should not see task checkboxes in the comment' do - expect(page).not_to have_selector( - 'li.note div.timeline-content input[type="checkbox"]' - ) - end - step 'I edit the last comment with a +1' do - find(".note").hover - find('.js-note-edit').click + page.within(".notes") do + find(".note").hover + find('.js-note-edit').click + end - within(".current-note-edit-form") do + page.within(".current-note-edit-form") do fill_in 'note[note]', with: '+1 Awesome!' click_button 'Save Comment' - sleep 0.05 end end step 'I should see +1 in the description' do - within(".note") do - page.should have_content("+1 Awesome!") + page.within(".note") do + expect(page).to have_content("+1 Awesome!") end end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index e3cf1b92cd..bb0cd9ac10 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -20,43 +20,43 @@ module SharedPaths # ---------------------------------------- step 'I visit group "Owned" page' do - visit group_path(Group.find_by(name:"Owned")) + visit group_path(Group.find_by(name: "Owned")) end step 'I visit group "Owned" issues page' do - visit issues_group_path(Group.find_by(name:"Owned")) + visit issues_group_path(Group.find_by(name: "Owned")) end step 'I visit group "Owned" merge requests page' do - visit merge_requests_group_path(Group.find_by(name:"Owned")) + visit merge_requests_group_path(Group.find_by(name: "Owned")) end step 'I visit group "Owned" members page' do - visit group_group_members_path(Group.find_by(name:"Owned")) + visit group_group_members_path(Group.find_by(name: "Owned")) end step 'I visit group "Owned" settings page' do - visit edit_group_path(Group.find_by(name:"Owned")) + visit edit_group_path(Group.find_by(name: "Owned")) end step 'I visit group "Guest" page' do - visit group_path(Group.find_by(name:"Guest")) + visit group_path(Group.find_by(name: "Guest")) end step 'I visit group "Guest" issues page' do - visit issues_group_path(Group.find_by(name:"Guest")) + visit issues_group_path(Group.find_by(name: "Guest")) end step 'I visit group "Guest" merge requests page' do - visit merge_requests_group_path(Group.find_by(name:"Guest")) + visit merge_requests_group_path(Group.find_by(name: "Guest")) end step 'I visit group "Guest" members page' do - visit group_group_members_path(Group.find_by(name:"Guest")) + visit group_group_members_path(Group.find_by(name: "Guest")) end step 'I visit group "Guest" settings page' do - visit edit_group_path(Group.find_by(name:"Guest")) + visit edit_group_path(Group.find_by(name: "Guest")) end # ---------------------------------------- @@ -92,7 +92,7 @@ module SharedPaths end step 'I should be redirected to the dashboard groups page' do - current_path.should == dashboard_groups_path + expect(current_path).to eq dashboard_groups_path end step 'I visit dashboard starred projects page' do @@ -123,12 +123,12 @@ module SharedPaths visit profile_keys_path end - step 'I visit profile design page' do - visit design_profile_path + step 'I visit profile preferences page' do + visit profile_preferences_path end - step 'I visit profile history page' do - visit history_profile_path + step 'I visit Audit Log page' do + visit audit_log_profile_path end # ---------------------------------------- @@ -139,6 +139,10 @@ module SharedPaths visit admin_root_path end + step 'I visit abuse reports page' do + visit admin_abuse_reports_path + end + step 'I visit admin projects page' do visit admin_namespaces_projects_path end @@ -201,11 +205,11 @@ module SharedPaths end step "I visit my project's commits page" do - visit namespace_project_commits_path(@project.namespace, @project, root_ref, {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref, { limit: 5 }) end step "I visit my project's commits page for a specific path" do - visit namespace_project_commits_path(@project.namespace, @project, root_ref + "/app/models/project.rb", {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref + "/app/models/project.rb", { limit: 5 }) end step 'I visit my project\'s commits stats page' do @@ -227,6 +231,10 @@ module SharedPaths visit namespace_project_merge_requests_path(@project.namespace, @project) end + step "I visit my project's members page" do + visit namespace_project_project_members_path(@project.namespace, @project) + end + step "I visit my project's wiki page" do visit namespace_project_wiki_path(@project.namespace, @project, :home) end @@ -247,6 +255,10 @@ module SharedPaths visit namespace_project_path(project.namespace, project) end + step 'I visit project "Shop" activity page' do + visit activity_namespace_project_path(project.namespace, project) + end + step 'I visit project "Forked Shop" merge requests page' do visit namespace_project_merge_requests_path(@forked_project.namespace, @forked_project) end @@ -268,11 +280,11 @@ module SharedPaths end step 'I visit project commits page' do - visit namespace_project_commits_path(@project.namespace, @project, root_ref, {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref, { limit: 5 }) end step 'I visit project commits page for stable branch' do - visit namespace_project_commits_path(@project.namespace, @project, 'stable', {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, 'stable', { limit: 5 }) end step 'I visit project source page' do @@ -288,11 +300,11 @@ module SharedPaths end step 'I am on the new file page' do - current_path.should eq(namespace_project_create_blob_path(@project.namespace, @project, root_ref)) + expect(current_path).to eq(namespace_project_create_blob_path(@project.namespace, @project, root_ref)) end step 'I am on the ".gitignore" edit file page' do - current_path.should eq(namespace_project_edit_blob_path( + expect(current_path).to eq(namespace_project_edit_blob_path( @project.namespace, @project, File.join(root_ref, '.gitignore'))) end @@ -323,16 +335,6 @@ module SharedPaths visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) end - step 'I visit issue page "Tasks-open"' do - issue = Issue.find_by(title: 'Tasks-open') - visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) - end - - step 'I visit issue page "Tasks-closed"' do - issue = Issue.find_by(title: 'Tasks-closed') - visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) - end - step 'I visit project "Shop" labels page' do project = Project.find_by(name: 'Shop') visit namespace_project_labels_path(project.namespace, project) @@ -363,13 +365,8 @@ module SharedPaths visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) end - step 'I visit merge request page "MR-task-open"' do - mr = MergeRequest.find_by(title: 'MR-task-open') - visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) - end - - step 'I visit merge request page "MR-task-closed"' do - mr = MergeRequest.find_by(title: 'MR-task-closed') + step 'I visit merge request page "Bug CO-01"' do + mr = MergeRequest.find_by(title: "Bug CO-01") visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) end @@ -434,13 +431,13 @@ module SharedPaths visit explore_projects_path end - step 'I visit the explore trending projects' do - visit trending_explore_projects_path - end + step 'I visit the explore trending projects' do + visit trending_explore_projects_path + end - step 'I visit the explore starred projects' do - visit starred_explore_projects_path - end + step 'I visit the explore starred projects' do + visit starred_explore_projects_path + end step 'I visit the public groups area' do visit explore_groups_path @@ -475,6 +472,6 @@ module SharedPaths # ---------------------------------------- step 'page status code should be 404' do - status_code.should == 404 + expect(status_code).to eq 404 end end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b60ac5e342..9ee2e5dfbe 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -14,6 +14,17 @@ module SharedProject @project.team << [@user, :master] end + step 'I disable snippets in project' do + @project.snippets_enabled = false + @project.save + end + + step 'I disable issues and merge requests in project' do + @project.issues_enabled = false + @project.merge_requests_enabled = false + @project.save + end + # Add another user to project "Shop" step 'I add a user to project "Shop"' do @project = Project.find_by(name: "Shop") @@ -68,13 +79,13 @@ module SharedProject step 'I should see project "Shop" activity feed' do project = Project.find_by(name: "Shop") - page.should have_content "#{@user.name} pushed new branch fix at #{project.name_with_namespace}" + expect(page).to have_content "#{@user.name} pushed new branch fix at #{project.name_with_namespace}" end step 'I should see project settings' do - current_path.should == edit_namespace_project_path(@project.namespace, @project) - page.should have_content("Project name") - page.should have_content("Features:") + expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project) + expect(page).to have_content("Project name") + expect(page).to have_content("Features:") end def current_project @@ -90,11 +101,11 @@ module SharedProject end step 'I should see project "Enterprise"' do - page.should have_content "Enterprise" + expect(page).to have_content "Enterprise" end step 'I should not see project "Enterprise"' do - page.should_not have_content "Enterprise" + expect(page).not_to have_content "Enterprise" end step 'internal project "Internal"' do @@ -102,11 +113,11 @@ module SharedProject end step 'I should see project "Internal"' do - page.should have_content "Internal" + expect(page).to have_content "Internal" end step 'I should not see project "Internal"' do - page.should_not have_content "Internal" + expect(page).not_to have_content "Internal" end step 'public project "Community"' do @@ -114,11 +125,11 @@ module SharedProject end step 'I should see project "Community"' do - page.should have_content "Community" + expect(page).to have_content "Community" end step 'I should not see project "Community"' do - page.should_not have_content "Community" + expect(page).not_to have_content "Community" end step '"John Doe" owns private project "Enterprise"' do diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index c5aed19331..c67e5e4a06 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -28,6 +28,10 @@ module SharedProjectTab ensure_active_main_tab('Issues') end + step 'the active main tab should be Members' do + ensure_active_main_tab('Members') + end + step 'the active main tab should be Merge Requests' do ensure_active_main_tab('Merge Requests') end @@ -41,8 +45,12 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - within '.nav-sidebar' do - page.should have_content('Back to project') + page.within '.nav-sidebar' do + expect(page).to have_content('Back to project') end end + + step 'the active main tab should be Activity' do + ensure_active_main_tab('Activity') + end end diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb index 209d77c7ac..fc1e8d6e88 100644 --- a/features/steps/shared/user.rb +++ b/features/steps/shared/user.rb @@ -2,16 +2,16 @@ module SharedUser include Spinach::DSL step 'User "John Doe" exists' do - user_exists("John Doe", {username: "john_doe"}) + user_exists("John Doe", { username: "john_doe" }) end step 'User "Mary Jane" exists' do - user_exists("Mary Jane", {username: "mary_jane"}) + user_exists("Mary Jane", { username: "mary_jane" }) end protected def user_exists(name, options = {}) - User.find_by(name: name) || create(:user, {name: name, admin: false}.merge(options)) + User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options)) end end diff --git a/features/steps/snippet_search.rb b/features/steps/snippet_search.rb index 669c7186c1..cf99987957 100644 --- a/features/steps/snippet_search.rb +++ b/features/steps/snippet_search.rb @@ -18,39 +18,39 @@ class Spinach::Features::SnippetSearch < Spinach::FeatureSteps end step 'I should see "line seven" in results' do - page.should have_content 'line seven' + expect(page).to have_content 'line seven' end step 'I should see "line four" in results' do - page.should have_content 'line four' + expect(page).to have_content 'line four' end step 'I should see "line ten" in results' do - page.should have_content 'line ten' + expect(page).to have_content 'line ten' end step 'I should not see "line eleven" in results' do - page.should_not have_content 'line eleven' + expect(page).not_to have_content 'line eleven' end step 'I should not see "line three" in results' do - page.should_not have_content 'line three' + expect(page).not_to have_content 'line three' end step 'I should see "Personal snippet one" in results' do - page.should have_content 'Personal snippet one' + expect(page).to have_content 'Personal snippet one' end step 'I should see "Personal snippet private" in results' do - page.should have_content 'Personal snippet private' + expect(page).to have_content 'Personal snippet private' end step 'I should not see "Personal snippet one" in results' do - page.should_not have_content 'Personal snippet one' + expect(page).not_to have_content 'Personal snippet one' end step 'I should not see "Personal snippet private" in results' do - page.should_not have_content 'Personal snippet private' + expect(page).not_to have_content 'Personal snippet private' end end diff --git a/features/steps/snippets/discover.rb b/features/steps/snippets/discover.rb index 2667c1e3d4..76379d09d0 100644 --- a/features/steps/snippets/discover.rb +++ b/features/steps/snippets/discover.rb @@ -4,15 +4,15 @@ class Spinach::Features::SnippetsDiscover < Spinach::FeatureSteps include SharedSnippet step 'I should see "Personal snippet one" in snippets' do - page.should have_content "Personal snippet one" + expect(page).to have_content "Personal snippet one" end step 'I should see "Personal snippet internal" in snippets' do - page.should have_content "Personal snippet internal" + expect(page).to have_content "Personal snippet internal" end step 'I should not see "Personal snippet private" in snippets' do - page.should_not have_content "Personal snippet private" + expect(page).not_to have_content "Personal snippet private" end def snippet diff --git a/features/steps/snippets/public_snippets.rb b/features/steps/snippets/public_snippets.rb index 67669dc0a6..2ebdca5ed3 100644 --- a/features/steps/snippets/public_snippets.rb +++ b/features/steps/snippets/public_snippets.rb @@ -4,11 +4,11 @@ class Spinach::Features::PublicSnippets < Spinach::FeatureSteps include SharedSnippet step 'I should see snippet "Personal snippet one"' do - page.should have_no_xpath("//i[@class='public-snippet']") + expect(page).to have_no_xpath("//i[@class='public-snippet']") end step 'I should see raw snippet "Personal snippet one"' do - page.should have_text(snippet.content) + expect(page).to have_text(snippet.content) end step 'I visit snippet page "Personal snippet one"' do diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index de936db85e..6ff48e0c6b 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -9,11 +9,11 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I should not see "Personal snippet one" in snippets' do - page.should_not have_content "Personal snippet one" + expect(page).not_to have_content "Personal snippet one" end step 'I click link "Edit"' do - within ".file-title" do + page.within ".file-title" do click_link "Edit" end end @@ -23,26 +23,38 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I submit new snippet "Personal snippet three"' do - fill_in "personal_snippet_title", :with => "Personal snippet three" - fill_in "personal_snippet_file_name", :with => "my_snippet.rb" - within('.file-editor') do + fill_in "personal_snippet_title", with: "Personal snippet three" + fill_in "personal_snippet_file_name", with: "my_snippet.rb" + page.within('.file-editor') do find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of snippet three' end click_button "Create snippet" end + step 'I submit new internal snippet' do + fill_in "personal_snippet_title", with: "Internal personal snippet one" + fill_in "personal_snippet_file_name", with: "my_snippet.rb" + choose 'personal_snippet_visibility_level_10' + + page.within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of internal snippet' + end + + click_button "Create snippet" + end + step 'I should see snippet "Personal snippet three"' do - page.should have_content "Personal snippet three" - page.should have_content "Content of snippet three" + expect(page).to have_content "Personal snippet three" + expect(page).to have_content "Content of snippet three" end step 'I submit new title "Personal snippet new title"' do - fill_in "personal_snippet_title", :with => "Personal snippet new title" + fill_in "personal_snippet_title", with: "Personal snippet new title" click_button "Save" end step 'I should see "Personal snippet new title"' do - page.should have_content "Personal snippet new title" + expect(page).to have_content "Personal snippet new title" end step 'I uncheck "Private" checkbox' do @@ -51,14 +63,22 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I should see "Personal snippet one" public' do - page.should have_no_xpath("//i[@class='public-snippet']") + expect(page).to have_no_xpath("//i[@class='public-snippet']") end step 'I visit snippet page "Personal snippet one"' do visit snippet_path(snippet) end + step 'I visit snippet page "Internal personal snippet one"' do + visit snippet_path(internal_snippet) + end + def snippet @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") end + + def internal_snippet + @snippet ||= PersonalSnippet.find_by!(title: "Internal personal snippet one") + end end diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb index 866f637ab6..007fcb2893 100644 --- a/features/steps/snippets/user.rb +++ b/features/steps/snippets/user.rb @@ -8,43 +8,43 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps end step 'I should see "Personal snippet one" in snippets' do - page.should have_content "Personal snippet one" + expect(page).to have_content "Personal snippet one" end step 'I should see "Personal snippet private" in snippets' do - page.should have_content "Personal snippet private" + expect(page).to have_content "Personal snippet private" end step 'I should see "Personal snippet internal" in snippets' do - page.should have_content "Personal snippet internal" + expect(page).to have_content "Personal snippet internal" end step 'I should not see "Personal snippet one" in snippets' do - page.should_not have_content "Personal snippet one" + expect(page).not_to have_content "Personal snippet one" end step 'I should not see "Personal snippet private" in snippets' do - page.should_not have_content "Personal snippet private" + expect(page).not_to have_content "Personal snippet private" end step 'I should not see "Personal snippet internal" in snippets' do - page.should_not have_content "Personal snippet internal" + expect(page).not_to have_content "Personal snippet internal" end step 'I click "Internal" filter' do - within('.nav-stacked') do + page.within('.nav-tabs') do click_link "Internal" end end step 'I click "Private" filter' do - within('.nav-stacked') do + page.within('.nav-tabs') do click_link "Private" end end step 'I click "Public" filter' do - within('.nav-stacked') do + page.within('.nav-tabs') do click_link "Public" end end diff --git a/features/steps/user.rb b/features/steps/user.rb index 10cae692a8..3230234cb6 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -28,13 +28,13 @@ class Spinach::Features::User < Spinach::FeatureSteps end step 'I should see contributed projects' do - within '.contributed-projects' do - page.should have_content(@contributed_project.name) + page.within '.contributed-projects' do + expect(page).to have_content(@contributed_project.name) end end step 'I should see contributions calendar' do - page.should have_css('.cal-heatmap-container') + expect(page).to have_css('.cal-heatmap-container') end def contributed_project diff --git a/features/support/capybara.rb b/features/support/capybara.rb new file mode 100644 index 0000000000..31dbf0feb2 --- /dev/null +++ b/features/support/capybara.rb @@ -0,0 +1,24 @@ +require 'spinach/capybara' +require 'capybara/poltergeist' + +# Give CI some extra time +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 + +Capybara.javascript_driver = :poltergeist +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) +end + +Spinach.hooks.on_tag("javascript") do + Capybara.current_driver = Capybara.javascript_driver +end + +Capybara.default_wait_time = timeout +Capybara.ignore_hidden_elements = false + +unless ENV['CI'] || ENV['CI_SERVER'] + require 'capybara-screenshot/spinach' + + # Keep only the screenshots generated from the last failing test suite + Capybara::Screenshot.prune_strategy = :keep_last_run +end diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb new file mode 100644 index 0000000000..1ab308cfa5 --- /dev/null +++ b/features/support/db_cleaner.rb @@ -0,0 +1,11 @@ +require 'database_cleaner' + +DatabaseCleaner.strategy = :truncation + +Spinach.hooks.before_scenario do + DatabaseCleaner.start +end + +Spinach.hooks.after_scenario do + DatabaseCleaner.clean +end diff --git a/features/support/env.rb b/features/support/env.rb index be17065ccf..672251af08 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -9,45 +9,23 @@ end ENV['RAILS_ENV'] = 'test' require './config/environment' -require 'rspec' require 'rspec/expectations' -require 'database_cleaner' -require 'spinach/capybara' require 'sidekiq/testing/inline' +require_relative 'capybara' +require_relative 'db_cleaner' + %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) end -Dir["#{Rails.root}/features/steps/shared/*.rb"].each {|file| require file} +Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } WebMock.allow_net_connect! -# -# JS driver -# -require 'capybara/poltergeist' -Capybara.javascript_driver = :poltergeist -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, js_errors: false, timeout: 90) -end -Spinach.hooks.on_tag("javascript") do - ::Capybara.current_driver = ::Capybara.javascript_driver -end -Capybara.default_wait_time = 60 -Capybara.ignore_hidden_elements = false - -DatabaseCleaner.strategy = :truncation - -Spinach.hooks.before_scenario do - DatabaseCleaner.start -end - -Spinach.hooks.after_scenario do - DatabaseCleaner.clean -end Spinach.hooks.before_run do include RSpec::Mocks::ExampleMethods + RSpec::Mocks.setup TestEnv.init(mailer: false) include FactoryGirl::Syntax::Methods diff --git a/lib/api/api.rb b/lib/api/api.rb index 60858a3940..eebd44ea5b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -19,7 +19,7 @@ module API message << " " << trace.join("\n ") API.logger.add Logger::FATAL, message - rack_response({ 'message' => '500 Internal Server Error' }, 500) + rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500) end format :json @@ -49,5 +49,6 @@ module API mount Namespaces mount Branches mount Labels + mount Settings end end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 0de4e720ff..f4efb651eb 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -32,7 +32,7 @@ module API # GET /projects/:id/repository/commits/:sha get ":id/repository/commits/:sha" do sha = params[:sha] - commit = user_project.repository.commit(sha) + commit = user_project.commit(sha) not_found! "Commit" unless commit present commit, with: Entities::RepoCommitDetail end @@ -46,7 +46,7 @@ module API # GET /projects/:id/repository/commits/:sha/diff get ":id/repository/commits/:sha/diff" do sha = params[:sha] - commit = user_project.repository.commit(sha) + commit = user_project.commit(sha) not_found! "Commit" unless commit commit.diffs end @@ -60,9 +60,9 @@ module API # GET /projects/:id/repository/commits/:sha/comments get ':id/repository/commits/:sha/comments' do sha = params[:sha] - commit = user_project.repository.commit(sha) + commit = user_project.commit(sha) not_found! 'Commit' unless commit - notes = Note.where(commit_id: commit.id) + notes = Note.where(commit_id: commit.id).order(:created_at) present paginate(notes), with: Entities::CommitNote end @@ -81,7 +81,7 @@ module API required_attributes! [:note] sha = params[:sha] - commit = user_project.repository.commit(sha) + commit = user_project.commit(sha) not_found! 'Commit' unless commit opts = { note: params[:note], diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 36332bc651..09d231af41 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -6,6 +6,10 @@ module API class UserBasic < UserSafe expose :id, :state, :avatar_url + + expose :web_url do |user, options| + Rails.application.routes.url_helpers.user_url(user) + end end class User < UserBasic @@ -20,16 +24,21 @@ module API class UserFull < User expose :email - expose :theme_id, :color_scheme_id, :projects_limit + expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project + expose :two_factor_enabled end class UserLogin < UserFull expose :private_token end + class Email < Grape::Entity + expose :id, :email + end + class Hook < Grape::Entity expose :id, :url, :created_at end @@ -58,6 +67,7 @@ module API expose :namespace expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } expose :avatar_url + expose :star_count, :forks_count end class ProjectMember < UserBasic @@ -68,6 +78,11 @@ module API class Group < Grape::Entity expose :id, :name, :path, :description + expose :avatar_url + + expose :web_url do |group, options| + Rails.application.routes.url_helpers.group_url(group) + end end class GroupDetail < Group @@ -170,6 +185,7 @@ module API expose :source_project_id, :target_project_id expose :label_names, as: :labels expose :description + expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone end @@ -189,6 +205,9 @@ module API expose :attachment_identifier, as: :attachment expose :author, using: Entities::UserBasic expose :created_at + expose :system?, as: :system + expose :upvote?, as: :upvote + expose :downvote?, as: :downvote end class MRNote < Grape::Entity @@ -251,11 +270,11 @@ module API class Compare < Grape::Entity expose :commit, using: Entities::RepoCommit do |compare, options| - Commit.decorate(compare.commits).last + Commit.decorate(compare.commits, nil).last end expose :commits, using: Entities::RepoCommit do |compare, options| - Commit.decorate(compare.commits) + Commit.decorate(compare.commits, nil) end expose :diffs, using: Entities::RepoDiff do |compare, options| @@ -276,5 +295,27 @@ module API class BroadcastMessage < Grape::Entity expose :message, :starts_at, :ends_at, :color, :font end + + class ApplicationSetting < Grape::Entity + expose :id + expose :default_projects_limit + expose :signup_enabled + expose :signin_enabled + expose :gravatar_enabled + expose :sign_in_text + expose :created_at + expose :updated_at + expose :home_page_url + expose :default_branch_protection + expose :twitter_sharing_enabled + expose :restricted_visibility_levels + expose :max_attachment_size + expose :session_expire_delay + expose :default_project_visibility + expose :default_snippet_visibility + expose :restricted_signup_domains + expose :user_oauth_applications + expose :after_sign_out_path + end end end diff --git a/lib/api/files.rb b/lib/api/files.rb index 3176ef0e25..83581cd399 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -34,7 +34,7 @@ module API ref = attrs.delete(:ref) file_path = attrs.delete(:file_path) - commit = user_project.repository.commit(ref) + commit = user_project.commit(ref) not_found! 'Commit' unless commit blob = user_project.repository.blob_at(commit.sha, file_path) @@ -47,7 +47,7 @@ module API file_path: blob.path, size: blob.size, encoding: "base64", - content: Base64.encode64(blob.data), + content: Base64.strict_encode64(blob.data), ref: ref, blob_id: blob.id, commit_id: commit.id, diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 8cb9f92097..024aeec2e1 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -20,7 +20,7 @@ module API present @groups, with: Entities::Group end - # Create group. Available only for admin + # Create group. Available only for users who can create groups. # # Parameters: # name (required) - The name of the group @@ -28,7 +28,7 @@ module API # Example Request: # POST /groups post do - authenticated_as_admin! + authorize! :create_group, current_user required_attributes! [:name, :path] attrs = attributes_for_keys [:name, :path, :description] @@ -62,7 +62,7 @@ module API delete ":id" do group = find_group(params[:id]) authorize! :admin_group, group - group.destroy + DestroyGroupService.new(group, current_user).execute end # Transfer a project to the Group namespace @@ -74,9 +74,9 @@ module API # POST /groups/:id/projects/:project_id post ":id/projects/:project_id" do authenticated_as_admin! - group = Group.find(params[:id]) + group = Group.find_by(id: params[:id]) project = Project.find(params[:project_id]) - result = ::Projects::TransferService.new(project, current_user, namespace_id: group.id).execute + result = ::Projects::TransferService.new(project, current_user).execute(group) if result present group diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index be133a2920..1ebf9a1f02 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -173,6 +173,10 @@ module API end end + def filter_by_iid(items, iid) + items.where(iid: iid) + end + # error helpers def forbidden!(reason = nil) @@ -239,7 +243,7 @@ module API end def secret_token - File.read(Rails.root.join('.gitlab_shell_secret')).chomp + File.read(Gitlab.config.gitlab_shell.secret_file).chomp end def handle_member_errors(errors) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index f98a17773e..e38736fc28 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -24,10 +24,6 @@ module API User.find_by(id: params[:user_id]) end - unless actor - return Gitlab::GitAccessStatus.new(false, 'No such user or key') - end - project_path = params[:project] # Check for *.wiki repositories. @@ -39,22 +35,14 @@ module API project = Project.find_with_namespace(project_path) - if project - access = - if wiki - Gitlab::GitAccessWiki.new(actor, project) - else - Gitlab::GitAccess.new(actor, project) - end + access = + if wiki + Gitlab::GitAccessWiki.new(actor, project) + else + Gitlab::GitAccess.new(actor, project) + end - status = access.check(params[:action], params[:changes]) - end - - if project && access.can_read_project? - status - else - Gitlab::GitAccessStatus.new(false, 'No such project') - end + access.check(params[:action], params[:changes]) end # diff --git a/lib/api/issues.rb b/lib/api/issues.rb index ff062be604..6e7a767207 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -51,6 +51,7 @@ module API # # Parameters: # id (required) - The ID of a project + # iid (optional) - Return the project issue having the given `iid` # state (optional) - Return "opened" or "closed" issues # labels (optional) - Comma-separated list of label names # milestone (optional) - Milestone title @@ -66,10 +67,12 @@ module API # GET /projects/:id/issues?labels=foo,bar&state=opened # GET /projects/:id/issues?milestone=1.0.0 # GET /projects/:id/issues?milestone=1.0.0&state=closed + # GET /issues?iid=42 get ":id/issues" do issues = user_project.issues issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? + issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? unless params[:milestone].nil? issues = filter_issues_milestone(issues, params[:milestone]) @@ -141,7 +144,7 @@ module API # PUT /projects/:id/issues/:issue_id put ":id/issues/:issue_id" do issue = user_project.issues.find(params[:issue_id]) - authorize! :modify_issue, issue + authorize! :update_issue, issue attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event] # Validate label names in advance @@ -154,7 +157,7 @@ module API if issue.valid? # Find or create labels and attach to issue. Labels are valid because # we already checked its name, so there can't be an error here - unless params[:labels].nil? + if params[:labels] && can?(current_user, :admin_issue, user_project) issue.remove_labels # Create and add labels to the new created issue issue.add_labels_by_names(params[:labels].split(',')) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index f3765f5ab0..ce21c699e8 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -24,6 +24,7 @@ module API # # Parameters: # id (required) - The ID of a project + # iid (optional) - Return the project MR having the given `iid` # state (optional) - Return requests "merged", "opened" or "closed" # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` @@ -36,11 +37,16 @@ module API # GET /projects/:id/merge_requests?order_by=updated_at # GET /projects/:id/merge_requests?sort=desc # GET /projects/:id/merge_requests?sort=asc + # GET /projects/:id/merge_requests?iid=42 # get ":id/merge_requests" do authorize! :read_merge_request, user_project merge_requests = user_project.merge_requests + unless params[:iid].nil? + merge_requests = filter_by_iid(merge_requests, params[:iid]) + end + merge_requests = case params["state"] when "opened" then merge_requests.opened @@ -103,7 +109,7 @@ module API # POST /projects/:id/merge_requests # post ":id/merge_requests" do - authorize! :write_merge_request, user_project + authorize! :create_merge_request, user_project required_attributes! [:source_branch, :target_branch, :title] attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description] @@ -131,7 +137,6 @@ module API # Parameters: # id (required) - The ID of a project # merge_request_id (required) - ID of MR - # source_branch - The source branch # target_branch - The target branch # assignee_id - Assignee user ID # title - Title of MR @@ -142,9 +147,14 @@ module API # PUT /projects/:id/merge_request/:merge_request_id # put ":id/merge_request/:merge_request_id" do - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description] + attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description] merge_request = user_project.merge_requests.find(params[:merge_request_id]) - authorize! :modify_merge_request, merge_request + authorize! :update_merge_request, merge_request + + # Ensure source_branch is not specified + if params[:source_branch].present? + render_api_error!('Source branch cannot be changed', 400) + end # Validate label names in advance if (errors = validate_label_params(params)).any? @@ -169,8 +179,8 @@ module API # Merge MR # # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR # merge_commit_message (optional) - Custom merge commit message # Example: # PUT /projects/:id/merge_request/:merge_request_id/merge @@ -186,7 +196,7 @@ module API merge_request.check_if_can_be_merged end - if merge_request.open? + if merge_request.open? && !merge_request.work_in_progress? if merge_request.can_be_merged? merge_request.automerge!(current_user, params[:merge_commit_message] || merge_request.merge_commit_message) present merge_request, with: Entities::MergeRequest @@ -195,7 +205,7 @@ module API end else # Merge request can not be merged - # because it is already closed/merged + # because it is already closed/merged or marked as WIP not_allowed! end else @@ -209,7 +219,7 @@ module API # Get a merge request's comments # # Parameters: - # id (required) - The ID of a project + # id (required) - The ID of a project # merge_request_id (required) - ID of MR # Examples: # GET /projects/:id/merge_request/:merge_request_id/comments @@ -219,15 +229,15 @@ module API authorize! :read_merge_request, merge_request - present paginate(merge_request.notes), with: Entities::MRNote + present paginate(merge_request.notes.fresh), with: Entities::MRNote end # Post comment to merge request # # Parameters: - # id (required) - The ID of a project + # id (required) - The ID of a project # merge_request_id (required) - ID of MR - # note (required) - Text of comment + # note (required) - Text of comment # Examples: # POST /projects/:id/merge_request/:merge_request_id/comments # diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index b90ed6af5f..50d3729449 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -1,10 +1,7 @@ module API # namespaces API class Namespaces < Grape::API - before do - authenticate! - authenticated_as_admin! - end + before { authenticate! } resource :namespaces do # Get a namespaces list @@ -12,7 +9,11 @@ module API # Example Request: # GET /namespaces get do - @namespaces = Namespace.all + @namespaces = if current_user.admin + Namespace.all + else + current_user.namespaces + end @namespaces = @namespaces.search(params[:search]) if params[:search].present? @namespaces = paginate @namespaces diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 3726be7c53..3efdfe2d46 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -78,17 +78,15 @@ module API put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do required_attributes! [:body] - authorize! :admin_note, user_project.notes.find(params[:note_id]) + note = user_project.notes.find(params[:note_id]) + + authorize! :admin_note, note opts = { - note: params[:body], - note_id: params[:note_id], - noteable_type: noteables_str.classify, - noteable_id: params[noteable_id_str] + note: params[:body] } - @note = ::Notes::UpdateService.new(user_project, current_user, - opts).execute + @note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note) if @note.valid? present @note, with: Entities::Note diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index be9850367b..ad4d2e65df 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -43,7 +43,8 @@ module API :push_events, :issues_events, :merge_requests_events, - :tag_push_events + :tag_push_events, + :note_events ] @hook = user_project.hooks.new(attrs) @@ -73,7 +74,8 @@ module API :push_events, :issues_events, :merge_requests_events, - :tag_push_events + :tag_push_events, + :note_events ] if @hook.update_attributes attrs diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 54f2555903..22ce3c6a06 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -46,7 +46,7 @@ module API # Example Request: # POST /projects/:id/snippets post ":id/snippets" do - authorize! :write_project_snippet, user_project + authorize! :create_project_snippet, user_project required_attributes! [:title, :file_name, :code, :visibility_level] attrs = attributes_for_keys [:title, :file_name, :visibility_level] @@ -74,7 +74,7 @@ module API # PUT /projects/:id/snippets/:snippet_id put ":id/snippets/:snippet_id" do @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_project_snippet, @snippet + authorize! :update_project_snippet, @snippet attrs = attributes_for_keys [:title, :file_name, :visibility_level] attrs[:content] = params[:code] if params[:code].present? @@ -98,7 +98,7 @@ module API delete ":id/snippets/:snippet_id" do begin @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_project_snippet, @snippet + authorize! :update_project_snippet, @snippet @snippet.destroy rescue not_found!('Snippet') diff --git a/lib/api/projects.rb b/lib/api/projects.rb index e3fff79d68..1f2251c9b9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -22,7 +22,12 @@ module API projects = projects.search(params[:search]) end - projects.reorder(project_order_by => project_sort) + if params[:ci_enabled_first].present? + projects.includes(:gitlab_ci_service). + reorder("services.active DESC, projects.#{project_order_by} #{project_sort}") + else + projects.reorder(project_order_by => project_sort) + end end def project_order_by diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 1fbf3dca3c..2d96c9666d 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -62,7 +62,7 @@ module API ref = params[:ref_name] || user_project.try(:default_branch) || 'master' path = params[:path] || nil - commit = user_project.repository.commit(ref) + commit = user_project.commit(ref) not_found!('Tree') unless commit tree = user_project.repository.tree(commit.id, path) diff --git a/lib/api/settings.rb b/lib/api/settings.rb new file mode 100644 index 0000000000..c885fcd7ea --- /dev/null +++ b/lib/api/settings.rb @@ -0,0 +1,35 @@ +module API + class Settings < Grape::API + before { authenticated_as_admin! } + + helpers do + def current_settings + @current_setting ||= + (ApplicationSetting.current || ApplicationSetting.create_from_defaults) + end + end + + # Get current applicaiton settings + # + # Example Request: + # GET /application/settings + get "application/settings" do + present current_settings, with: Entities::ApplicationSetting + end + + # Modify applicaiton settings + # + # Example Request: + # PUT /application/settings + put "application/settings" do + attributes = current_settings.attributes.keys - ["id"] + attrs = attributes_for_keys(attributes) + + if current_settings.update_attributes(attrs) + present current_settings, with: Entities::ApplicationSetting + else + render_validation_error!(current_settings) + end + end + end +end diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 518964db50..22b8f90dc5 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -47,7 +47,7 @@ module API owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data) + @hook.execute(data, 'system_hooks') data end diff --git a/lib/api/users.rb b/lib/api/users.rb index 032a5d76e4..ee29f95224 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -131,11 +131,11 @@ module API # Add ssh key to a specified user. Only available to admin users. # # Parameters: - # id (required) - The ID of a user - # key (required) - New SSH Key - # title (required) - New SSH Key's title + # id (required) - The ID of a user + # key (required) - New SSH Key + # title (required) - New SSH Key's title # Example Request: - # POST /users/:id/keys + # POST /users/:id/keys post ":id/keys" do authenticated_as_admin! required_attributes! [:title, :key] @@ -153,9 +153,9 @@ module API # Get ssh keys of a specified user. Only available to admin users. # # Parameters: - # uid (required) - The ID of a user + # uid (required) - The ID of a user # Example Request: - # GET /users/:uid/keys + # GET /users/:uid/keys get ':uid/keys' do authenticated_as_admin! user = User.find_by(id: params[:uid]) @@ -185,6 +185,65 @@ module API end end + # Add email to a specified user. Only available to admin users. + # + # Parameters: + # id (required) - The ID of a user + # email (required) - Email address + # Example Request: + # POST /users/:id/emails + post ":id/emails" do + authenticated_as_admin! + required_attributes! [:email] + + user = User.find(params[:id]) + attrs = attributes_for_keys [:email] + email = user.emails.new attrs + if email.save + NotificationService.new.new_email(email) + present email, with: Entities::Email + else + render_validation_error!(email) + end + end + + # Get emails of a specified user. Only available to admin users. + # + # Parameters: + # uid (required) - The ID of a user + # Example Request: + # GET /users/:uid/emails + get ':uid/emails' do + authenticated_as_admin! + user = User.find_by(id: params[:uid]) + not_found!('User') unless user + + present user.emails, with: Entities::Email + end + + # Delete existing email of a specified user. Only available to admin + # users. + # + # Parameters: + # uid (required) - The ID of a user + # id (required) - Email ID + # Example Request: + # DELETE /users/:uid/emails/:id + delete ':uid/emails/:id' do + authenticated_as_admin! + user = User.find_by(id: params[:uid]) + not_found!('User') unless user + + begin + email = user.emails.find params[:id] + email.destroy + + user.update_secondary_emails! + rescue ActiveRecord::RecordNotFound + not_found!('Email') + end + end + # Delete user. Available only for admin # # Example Request: @@ -194,7 +253,37 @@ module API user = User.find_by(id: params[:id]) if user - user.destroy + DeleteUserService.new(current_user).execute(user) + else + not_found!('User') + end + end + + # Block user. Available only for admin + # + # Example Request: + # PUT /users/:id/block + put ':id/block' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + + if user + user.block + else + not_found!('User') + end + end + + # Unblock user. Available only for admin + # + # Example Request: + # PUT /users/:id/unblock + put ':id/unblock' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + + if user + user.activate else not_found!('User') end @@ -259,6 +348,58 @@ module API rescue end end + + # Get currently authenticated user's emails + # + # Example Request: + # GET /user/emails + get "emails" do + present current_user.emails, with: Entities::Email + end + + # Get single email owned by currently authenticated user + # + # Example Request: + # GET /user/emails/:id + get "emails/:id" do + email = current_user.emails.find params[:id] + present email, with: Entities::Email + end + + # Add new email to currently authenticated user + # + # Parameters: + # email (required) - Email address + # Example Request: + # POST /user/emails + post "emails" do + required_attributes! [:email] + + attrs = attributes_for_keys [:email] + email = current_user.emails.new attrs + if email.save + NotificationService.new.new_email(email) + present email, with: Entities::Email + else + render_validation_error!(email) + end + end + + # Delete existing email of currently authenticated user + # + # Parameters: + # id (required) - EMail ID + # Example Request: + # DELETE /user/emails/:id + delete "emails/:id" do + begin + email = current_user.emails.find params[:id] + email.destroy + + current_user.update_secondary_emails! + rescue + end + end end end end diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 9ab6aca276..ce75476a09 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -7,34 +7,50 @@ module Backup def initialize @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] @db_dir = File.join(Gitlab.config.backup.path, 'db') - FileUtils.mkdir_p(@db_dir) unless Dir.exists?(@db_dir) end def dump + FileUtils.rm_rf(@db_dir) + # Ensure the parent dir of @db_dir exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create @db_dir before us + FileUtils.mkdir(@db_dir, mode: 0700) + success = case config["adapter"] when /^mysql/ then $progress.print "Dumping MySQL database #{config['database']} ... " + # Workaround warnings from MySQL 5.6 about passwords on cmd line + ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] system('mysqldump', *mysql_args, config['database'], out: db_file_name) when "postgresql" then $progress.print "Dumping PostgreSQL database #{config['database']} ... " pg_env - system('pg_dump', config['database'], out: db_file_name) + # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. + system('pg_dump', '--clean', config['database'], out: db_file_name) end report_success(success) abort 'Backup failed' unless success + + $progress.print 'Compressing database ... ' + success = system('gzip', db_file_name) + report_success(success) + abort 'Backup failed: compress error' unless success end def restore + $progress.print 'Decompressing database ... ' + success = system('gzip', '-d', db_file_name_gz) + report_success(success) + abort 'Restore failed: decompress error' unless success + success = case config["adapter"] when /^mysql/ then $progress.print "Restoring MySQL database #{config['database']} ... " + # Workaround warnings from MySQL 5.6 about passwords on cmd line + ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] system('mysql', *mysql_args, config['database'], in: db_file_name) when "postgresql" then $progress.print "Restoring PostgreSQL database #{config['database']} ... " - # Drop all tables because PostgreSQL DB dumps do not contain DROP TABLE - # statements like MySQL. - Rake::Task["gitlab:db:drop_all_tables"].invoke - Rake::Task["gitlab:db:drop_all_postgres_sequences"].invoke pg_env system('psql', config['database'], '-f', db_file_name) end @@ -48,14 +64,17 @@ module Backup File.join(db_dir, 'database.sql') end + def db_file_name_gz + File.join(db_dir, 'database.sql.gz') + end + def mysql_args args = { 'host' => '--host', 'port' => '--port', 'socket' => '--socket', 'username' => '--user', - 'encoding' => '--default-character-set', - 'password' => '--password' + 'encoding' => '--default-character-set' } args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index b69aebf9fe..13c68d9354 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -16,18 +16,16 @@ module Backup file << s.to_yaml.gsub(/^---\n/,'') end - FileUtils.chmod(0700, folders_to_backup) - # create archive $progress.print "Creating backup archive: #{tar_file} ... " - orig_umask = File.umask(0077) - if Kernel.system('tar', '-cf', tar_file, *backup_contents) + # Set file permissions on open to prevent chmod races. + tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]} + if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options) $progress.puts "done".green else puts "creating archive #{tar_file} failed".red abort 'Backup failed' end - File.umask(orig_umask) upload(tar_file) end @@ -46,7 +44,8 @@ module Backup connection = ::Fog::Storage.new(connection_settings) directory = connection.directories.get(remote_directory) - if directory.files.create(key: tar_file, body: File.open(tar_file), public: false) + if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, + multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size) $progress.puts "done".green else puts "uploading backup to #{remote_directory} failed".red diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index dfb2da9f84..4d70f7883d 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -130,7 +130,10 @@ module Backup def prepare FileUtils.rm_rf(backup_repos_path) - FileUtils.mkdir_p(backup_repos_path) + # Ensure the parent dir of backup_repos_path exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create backup_repos_path before us + FileUtils.mkdir(backup_repos_path, mode: 0700) end def silent diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index e50e1ff4f1..1f9626644e 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -10,7 +10,11 @@ module Backup # Copy uploads from public/uploads to backup/uploads def dump - FileUtils.mkdir_p(backup_uploads_dir) + FileUtils.rm_rf(backup_uploads_dir) + # Ensure the parent dir of backup_uploads_dir exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create backup_uploads_dir before us + FileUtils.mkdir(backup_uploads_dir, mode: 0700) FileUtils.cp_r(app_uploads_dir, backup_dir) end @@ -23,7 +27,7 @@ module Backup def backup_existing_uploads_dir timestamped_uploads_path = File.join(app_uploads_dir, '..', "uploads.#{Time.now.to_i}") if File.exists?(app_uploads_dir) - FileUtils.mv(app_uploads_dir, timestamped_uploads_path) + FileUtils.mv(app_uploads_dir, File.expand_path(timestamped_uploads_path)) end end end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 6e4ed01e07..322aed5e27 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -55,12 +55,16 @@ module ExtractsPath valid_refs = @project.repository.ref_names valid_refs.select! { |v| id.start_with?("#{v}/") } - if valid_refs.length != 1 + if valid_refs.length == 0 # No exact ref match, so just try our best pair = id.match(/([^\/]+)(.*)/).captures else + # There is a distinct possibility that multiple refs prefix the ID. + # Use the longest match to maximize the chance that we have the + # right ref. + best_match = valid_refs.max_by(&:length) # Partition the string into the ref and the path, ignoring the empty first value - pair = id.partition(valid_refs.first)[1..-1] + pair = id.partition(best_match)[1..-1] end end @@ -90,7 +94,7 @@ module ExtractsPath @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? } @options = HashWithIndifferentAccess.new(@options) - @id = get_id + @id = Addressable::URI.unescape(get_id) @ref, @path = extract_ref(@id) @repo = @project.repository if @options[:extended_sha1].blank? diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 424541b4a0..6d0e30e916 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -51,9 +51,9 @@ module Gitlab def protection_options { - "Not protected, developers and masters can (force) push and delete the branch" => PROTECTION_NONE, - "Partially protected, developers can also push but prevent all force pushes and deletion" => PROTECTION_DEV_CAN_PUSH, - "Fully protected, only masters can push and prevent all force pushes and deletion" => PROTECTION_FULL, + "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, + "Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH, + "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL, } end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb new file mode 100644 index 0000000000..bf33e5b1b1 --- /dev/null +++ b/lib/gitlab/asciidoc.rb @@ -0,0 +1,60 @@ +require 'asciidoctor' +require 'html/pipeline' + +module Gitlab + # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters + # the resulting HTML through HTML pipeline filters. + module Asciidoc + + # Provide autoload paths for filters to prevent a circular dependency error + autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' + + DEFAULT_ADOC_ATTRS = [ + 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', + 'env-gitlab', 'source-highlighter=html-pipeline' + ].freeze + + # Public: Converts the provided Asciidoc markup into HTML. + # + # input - the source text in Asciidoc format + # context - a Hash with the template context: + # :commit + # :project + # :project_wiki + # :requested_path + # :ref + # asciidoc_opts - a Hash of options to pass to the Asciidoctor converter + # html_opts - a Hash of options for HTML output: + # :xhtml - output XHTML instead of HTML + # + def self.render(input, context, asciidoc_opts = {}, html_opts = {}) + asciidoc_opts = asciidoc_opts.reverse_merge( + safe: :secure, + backend: html_opts[:xhtml] ? :xhtml5 : :html5, + attributes: [] + ) + asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) + + html = ::Asciidoctor.convert(input, asciidoc_opts) + + if context[:project] + result = HTML::Pipeline.new(filters).call(html, context) + + save_opts = html_opts[:xhtml] ? + Nokogiri::XML::Node::SaveOptions::AS_XHTML : 0 + + html = result[:output].to_html(save_with: save_opts) + end + + html.html_safe + end + + private + + def self.filters + [ + Gitlab::Markdown::RelativeLinkFilter + ] + end + end +end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 050b5ba29d..12292f614e 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -1,4 +1,3 @@ -require_relative 'rack_attack_helpers' require_relative 'shell_env' module Grack @@ -27,7 +26,12 @@ module Grack auth! if project && authorized_request? - @app.call(env) + if ENV['GITLAB_GRACK_AUTH_ONLY'] == '1' + # Tell gitlab-git-http-server the request is OK, and what the GL_ID is + render_grack_auth_ok + else + @app.call(env) + end elsif @user.nil? && !@gitlab_ci unauthorized else @@ -175,6 +179,10 @@ module Grack end end + def render_grack_auth_ok + [200, { "Content-Type" => "application/json" }, [JSON.dump({ 'GL_ID' => Gitlab::ShellEnv.gl_id(@user) })]] + end + def render_not_found [404, { "Content-Type" => "text/plain" }, ["Not Found"]] end diff --git a/lib/gitlab/backend/rack_attack_helpers.rb b/lib/gitlab/backend/rack_attack_helpers.rb deleted file mode 100644 index 8538f3f6ec..0000000000 --- a/lib/gitlab/backend/rack_attack_helpers.rb +++ /dev/null @@ -1,31 +0,0 @@ -# rack-attack v4.2.0 doesn't yet support clearing of keys. -# Taken from https://github.com/kickstarter/rack-attack/issues/113 -class Rack::Attack::Allow2Ban - def self.reset(discriminator, options) - findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option" - - cache.reset_count("#{key_prefix}:count:#{discriminator}", findtime) - cache.delete("#{key_prefix}:ban:#{discriminator}") - end -end - -class Rack::Attack::Cache - def reset_count(unprefixed_key, period) - epoch_time = Time.now.to_i - # Add 1 to expires_in to avoid timing error: http://git.io/i1PHXA - expires_in = period - (epoch_time % period) + 1 - key = "#{(epoch_time / period).to_i}:#{unprefixed_key}" - delete(key) - end - - def delete(unprefixed_key) - store.delete("#{prefix}:#{unprefixed_key}") - end -end - -class Rack::Attack::StoreProxy::RedisStoreProxy - def delete(key, options={}) - self.del(key) - rescue Redis::BaseError - end -end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 530f9d93de..172d4902ad 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -244,6 +244,16 @@ module Gitlab end end + # Check if such directory exists in repositories. + # + # Usage: + # exists?('gitlab') + # exists?('gitlab/cookies.git') + # + def exists?(dir_name) + File.exists?(full_path(dir_name)) + end + protected def gitlab_shell_path @@ -264,10 +274,6 @@ module Gitlab File.join(repos_path, dir_name) end - def exists?(dir_name) - File.exists?(full_path(dir_name)) - end - def gitlab_shell_projects_path File.join(gitlab_shell_path, 'bin', 'gitlab-projects') end diff --git a/lib/gitlab/backend/shell_env.rb b/lib/gitlab/backend/shell_env.rb index 044afb27f3..9f5adee594 100644 --- a/lib/gitlab/backend/shell_env.rb +++ b/lib/gitlab/backend/shell_env.rb @@ -6,12 +6,23 @@ module Gitlab def set_env(user) # Set GL_ID env variable - ENV['GL_ID'] = "user-#{user.id}" + if user + ENV['GL_ID'] = gl_id(user) + end end def reset_env # Reset GL_ID env variable ENV['GL_ID'] = nil end + + def gl_id(user) + if user.present? + "user-#{user.id}" + else + # This empty string is used in the render_grack_auth_ok method + "" + end + end end end diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 5b1952b967..aec44b8c87 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -1,6 +1,8 @@ module Gitlab module BitbucketImport class Client + class Unauthorized < StandardError; end + attr_reader :consumer, :api def initialize(access_token = nil, access_token_secret = nil) @@ -46,23 +48,23 @@ module Gitlab end def user - JSON.parse(api.get("/api/1.0/user").body) + JSON.parse(get("/api/1.0/user").body) end def issues(project_identifier) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues").body) end def issue_comments(project_identifier, issue_id) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) end def project(project_identifier) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body) end def find_deploy_key(project_identifier, key) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| deploy_key["key"].chomp == key.chomp end end @@ -82,11 +84,22 @@ module Gitlab end def projects - JSON.parse(api.get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } + JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } + end + + def incompatible_projects + JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" } end private + def get(url) + response = api.get(url) + raise Unauthorized if (400..499).include?(response.code.to_i) + + response + end + def config Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"} end diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index ab184d95c0..aeec595782 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -8,7 +8,7 @@ module Gitlab def closed_by_message(message) return [] if message.nil? - + closing_statements = message.scan(ISSUE_CLOSING_REGEX). map { |ref| ref[0] }.join(" ") diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 3fd0823df0..45bb904ed7 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -17,7 +17,7 @@ module Gitlab events = Event.reorder(nil).contributions.where(author_id: user.id). where("created_at > ?", date_from).where(project_id: projects). group('date(created_at)'). - select('date(created_at), count(id) as total_amount'). + select('date(created_at) as date, count(id) as total_amount'). map(&:attributes) dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index d8f696d247..931d51c55d 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -21,7 +21,8 @@ module Gitlab gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], - max_attachment_size: Settings.gitlab['max_attachment_size'] + max_attachment_size: Settings.gitlab['max_attachment_size'], + session_expire_delay: Settings.gitlab['session_expire_delay'] ) end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index bc72b7528d..c90184d31c 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -31,8 +31,7 @@ module Gitlab def can_push_to_branch?(ref) return false unless user - if project.protected_branch?(ref) && - !(project.developers_can_push_to_protected_branch?(ref) && project.team.developer?(user)) + if project.protected_branch?(ref) && !project.developers_can_push_to_protected_branch?(ref) user.can?(:push_code_to_protected_branches, project) else user.can?(:push_code, project) @@ -50,13 +49,25 @@ module Gitlab end def check(cmd, changes = nil) + unless actor + return build_status_object(false, "No user or key was provided.") + end + + if user && !user_allowed? + return build_status_object(false, "Your account has been blocked.") + end + + unless project && can_read_project? + return build_status_object(false, 'The project you were looking for could not be found.') + end + case cmd when *DOWNLOAD_COMMANDS download_access_check when *PUSH_COMMANDS push_access_check(changes) else - build_status_object(false, "Wrong command") + build_status_object(false, "The command you're trying to execute is not allowed.") end end @@ -64,7 +75,7 @@ module Gitlab if user user_download_access_check elsif deploy_key - deploy_key_download_access_check + build_status_object(true) else raise 'Wrong actor' end @@ -74,39 +85,27 @@ module Gitlab if user user_push_access_check(changes) elsif deploy_key - build_status_object(false, "Deploy key not allowed to push") + build_status_object(false, "Deploy keys are not allowed to push code.") else raise 'Wrong actor' end end def user_download_access_check - if user && user_allowed? && user.can?(:download_code, project) - build_status_object(true) - else - build_status_object(false, "You don't have access") + unless user.can?(:download_code, project) + return build_status_object(false, "You are not allowed to download code from this project.") end - end - def deploy_key_download_access_check - if can_read_project? - build_status_object(true) - else - build_status_object(false, "Deploy key not allowed to access this project") - end + build_status_object(true) end def user_push_access_check(changes) - unless user && user_allowed? - return build_status_object(false, "You don't have access") - end - if changes.blank? return build_status_object(true) end unless project.repository.exists? - return build_status_object(false, "Repository does not exist") + return build_status_object(false, "A repository for this project does not exist yet.") end changes = changes.lines if changes.kind_of?(String) @@ -136,11 +135,24 @@ module Gitlab :push_code end - if user.can?(action, project) - build_status_object(true) - else - build_status_object(false, "You don't have permission") + unless user.can?(action, project) + status = + case action + when :force_push_code_to_protected_branches + build_status_object(false, "You are not allowed to force push code to a protected branch on this project.") + when :remove_protected_branches + build_status_object(false, "You are not allowed to deleted protected branches from this project.") + when :push_code_to_protected_branches + build_status_object(false, "You are not allowed to push code to protected branches on this project.") + when :admin_project + build_status_object(false, "You are not allowed to change existing tags on this project.") + else # :push_code + build_status_object(false, "You are not allowed to push code to this project.") + end + return status end + + build_status_object(true) end def forced_push?(oldrev, newrev) diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index 73d99b9620..8672cbc0ec 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -1,10 +1,10 @@ module Gitlab class GitAccessWiki < GitAccess def change_access_check(change) - if user.can?(:write_wiki, project) + if user.can?(:create_wiki, project) build_status_object(true) else - build_status_object(false, "You don't have access") + build_status_object(false, "You are not allowed to write to this project's wiki.") end end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 23832b3233..98039a76dc 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -11,7 +11,9 @@ module Gitlab def execute #Issues && Comments - client.list_issues(project.import_source, state: :all).each do |issue| + client.list_issues(project.import_source, state: :all, + sort: :created, + direction: :asc).each do |issue| if issue.pull_request.nil? body = @formatter.author_line(issue.user.login, issue.body) diff --git a/lib/gitlab/gitorious_import.rb b/lib/gitlab/gitorious_import.rb new file mode 100644 index 0000000000..8d0132a744 --- /dev/null +++ b/lib/gitlab/gitorious_import.rb @@ -0,0 +1,5 @@ +module Gitlab + module GitoriousImport + GITORIOUS_HOST = "https://gitorious.org" + end +end diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb index 8cdc3d4afa..99fe5bdebf 100644 --- a/lib/gitlab/gitorious_import/client.rb +++ b/lib/gitlab/gitorious_import/client.rb @@ -1,7 +1,5 @@ module Gitlab module GitoriousImport - GITORIOUS_HOST = "https://gitorious.org" - class Client attr_reader :repo_list @@ -14,7 +12,7 @@ module Gitlab end def repos - @repos ||= repo_names.map { |full_name| Repository.new(full_name) } + @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) } end def repo(id) diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb index f702797dc6..c88f1ae358 100644 --- a/lib/gitlab/gitorious_import/repository.rb +++ b/lib/gitlab/gitorious_import/repository.rb @@ -1,7 +1,5 @@ module Gitlab module GitoriousImport - GITORIOUS_HOST = "https://gitorious.org" - Repository = Struct.new(:full_name) do def id Digest::SHA1.hexdigest(full_name) diff --git a/lib/gitlab/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb index 02f31e45f8..890bd9a355 100644 --- a/lib/gitlab/google_code_import/client.rb +++ b/lib/gitlab/google_code_import/client.rb @@ -21,6 +21,10 @@ module Gitlab @repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.select(&:git?) end + def incompatible_repos + @incompatible_repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.reject(&:git?) + end + def repo(id) repos.find { |repo| repo.id == id } end diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 70bfe05977..03c410726a 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -327,7 +327,7 @@ module Gitlab link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}" text = "[#{filename}](#{link})" - text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/ + text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/i text end.compact end diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb index 6e4de197ee..3e5d728f3b 100644 --- a/lib/gitlab/identifier.rb +++ b/lib/gitlab/identifier.rb @@ -5,7 +5,7 @@ module Gitlab def identify(identifier, project, newrev) if identifier.blank? # Local push from gitlab - email = project.repository.commit(newrev).author_email rescue nil + email = project.commit(newrev).author_email rescue nil User.find_by(email: email) if email elsif identifier =~ /\Auser-\d+\Z/ diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb index 3517ecdf5c..99e7b529ba 100644 --- a/lib/gitlab/inline_diff.rb +++ b/lib/gitlab/inline_diff.rb @@ -46,8 +46,11 @@ module Gitlab end last_the_same_symbols += 1 last_token = first_line[last_the_same_symbols..-1] - diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, FINISH + last_token) - diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, FINISH + last_token) + # This is tricky: escape backslashes so that `sub` doesn't interpret them + # as backreferences. Regexp.escape does NOT do the right thing. + replace_token = FINISH + last_token.gsub(/\\/, '\&\&') + diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token) + diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token) end diff_arr end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 960fb3849b..16ff03c38d 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -40,7 +40,7 @@ module Gitlab user.block unless user.blocked? false else - user.activate if user.blocked? + user.activate if user.blocked? && !ldap_config.block_auto_created_users true end else diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb index 649cf3194b..bad683c651 100644 --- a/lib/gitlab/ldap/authentication.rb +++ b/lib/gitlab/ldap/authentication.rb @@ -1,4 +1,4 @@ -# This calls helps to authenticate to LDAP by providing username and password +# These calls help to authenticate to LDAP by providing username and password # # Since multiple LDAP servers are supported, it will loop through all of them # until a valid bind is found diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 47c456d8dc..9f6e19a09f 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -1,57 +1,35 @@ require 'html/pipeline' -require 'html/pipeline/gitlab' module Gitlab # Custom parser for GitLab-flavored Markdown # - # It replaces references in the text with links to the appropriate items in - # GitLab. - # - # Supported reference formats are: - # * @foo for team members - # * #123 for issues - # * #JIRA-123 for Jira issues - # * !123 for merge requests - # * $123 for snippets - # * 123456 for commits - # * 123456...7890123 for commit ranges (comparisons) - # - # It also parses Emoji codes to insert images. See - # http://www.emoji-cheat-sheet.com/ for a list of the supported icons. - # - # Examples - # - # >> gfm("Hey @david, can you fix this?") - # => "Hey @david, can you fix this?" - # - # >> gfm("Commit 35d5f7c closes #1234") - # => "Commit 35d5f7c closes #1234" - # - # >> gfm(":trollface:") - # => "\":trollface:\" + # See the files in `lib/gitlab/markdown/` for specific processing information. module Markdown - include IssuesHelper - - attr_reader :options, :html_options + # Provide autoload paths for filters to prevent a circular dependency error + autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' + autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' + autoload :CommitReferenceFilter, 'gitlab/markdown/commit_reference_filter' + autoload :EmojiFilter, 'gitlab/markdown/emoji_filter' + autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter' + autoload :ExternalLinkFilter, 'gitlab/markdown/external_link_filter' + autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' + autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' + autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' + autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' + autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' + autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' + autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter' + autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' + autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # project - the project + # options - A Hash of options used to customize output (default: {}): + # :xhtml - output XHTML instead of HTML + # :reference_only_path - Use relative path for reference links # html_options - extra options for the reference links as given to link_to - def gfm(text, project = @project, html_options = {}) - gfm_with_options(text, {}, project, html_options) - end - - # Public: Parse the provided text with GitLab-Flavored Markdown - # - # text - the source text - # options - parse_tasks - render tasks - # - xhtml - output XHTML instead of HTML - # - reference_only_path - Use relative path for reference links - # project - the project - # html_options - extra options for the reference links as given to link_to - def gfm_with_options(text, options = {}, project = @project, html_options = {}) + def gfm(text, options = {}, html_options = {}) return text if text.nil? # Duplicate the string so we don't alter the original, then call to_str @@ -60,52 +38,38 @@ module Gitlab text = text.dup.to_str options.reverse_merge!( - parse_tasks: false, xhtml: false, - reference_only_path: true + reference_only_path: true, + project: @project, + current_user: current_user ) - @options = options - @html_options = html_options + @pipeline ||= HTML::Pipeline.new(filters) - # TODO: add popups with additional information + context = { + # SanitizationFilter + pipeline: options[:pipeline], - # Used markdown pipelines in GitLab: - # GitlabEmojiFilter - performs emoji replacement. - # SanitizationFilter - remove unsafe HTML tags and attributes - # - # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters - filters = [ - HTML::Pipeline::Gitlab::GitlabEmojiFilter, - HTML::Pipeline::SanitizationFilter - ] + # EmojiFilter + asset_root: Gitlab.config.gitlab.url, + asset_host: Gitlab::Application.config.asset_host, - whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST - whitelist[:attributes][:all].push('class', 'id') - whitelist[:elements].push('span') + # TableOfContentsFilter + no_header_anchors: options[:no_header_anchors], - # Remove the rel attribute that the sanitize gem adds, and remove the - # href attribute if it contains inline javascript - fix_anchors = lambda do |env| - name, node = env[:node_name], env[:node] - if name == 'a' - node.remove_attribute('rel') - if node['href'] && node['href'].match('javascript:') - node.remove_attribute('href') - end - end - end - whitelist[:transformers].push(fix_anchors) + # ReferenceFilter + current_user: options[:current_user], + only_path: options[:reference_only_path], + project: options[:project], + reference_class: html_options[:class], - markdown_context = { - asset_root: Gitlab.config.gitlab.url, - asset_host: Gitlab::Application.config.asset_host, - whitelist: whitelist + # RelativeLinkFilter + ref: @ref, + requested_path: @path, + project_wiki: @project_wiki } - markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline - - result = markdown_pipeline.call(text, markdown_context) + result = @pipeline.call(text, context) save_options = 0 if options[:xhtml] @@ -114,286 +78,38 @@ module Gitlab text = result[:output].to_html(save_with: save_options) - # Extract pre blocks so they are not altered - # from http://github.github.com/github-flavored-markdown/ - text.gsub!(%r{
    .*?
    |.*?}m) { |match| extract_piece(match) } - # Extract links with probably parsable hrefs - text.gsub!(%r{.*?}m) { |match| extract_piece(match) } - # Extract images with probably parsable src - text.gsub!(%r{}m) { |match| extract_piece(match) } - - text = parse(text, project) - - # Insert pre block extractions - text.gsub!(/\{gfm-extraction-(\h{32})\}/) do - insert_piece($1) - end - - if options[:parse_tasks] - text = parse_tasks(text) - end - text.html_safe end private - def extract_piece(text) - @extractions ||= {} - - md5 = Digest::MD5.hexdigest(text) - @extractions[md5] = text - "{gfm-extraction-#{md5}}" - end - - def insert_piece(id) - @extractions[id] - end - - # Private: Parses text for references + # Filters used in our pipeline # - # text - Text to parse + # SanitizationFilter should come first so that all generated reference HTML + # goes through untouched. # - # Returns parsed text - def parse(text, project = @project) - parse_references(text, project) if project + # See https://github.com/jch/html-pipeline#filters for more filters. + def filters + [ + Gitlab::Markdown::SanitizationFilter, - text - end + Gitlab::Markdown::RelativeLinkFilter, + Gitlab::Markdown::EmojiFilter, + Gitlab::Markdown::TableOfContentsFilter, + Gitlab::Markdown::AutolinkFilter, + Gitlab::Markdown::ExternalLinkFilter, - NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR - PROJ_STR = "(?#{NAME_STR}/#{NAME_STR})" + Gitlab::Markdown::UserReferenceFilter, + Gitlab::Markdown::IssueReferenceFilter, + Gitlab::Markdown::ExternalIssueReferenceFilter, + Gitlab::Markdown::MergeRequestReferenceFilter, + Gitlab::Markdown::SnippetReferenceFilter, + Gitlab::Markdown::CommitRangeReferenceFilter, + Gitlab::Markdown::CommitReferenceFilter, + Gitlab::Markdown::LabelReferenceFilter, - REFERENCE_PATTERN = %r{ - (?\W)? # Prefix - ( # Reference - @(?#{NAME_STR}) # User name - |~(?