From f0b3092d2f7e0a177445727e6c6bd461c19b6ba7 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 19 May 2014 11:52:02 +0200 Subject: [PATCH 01/68] Recommend issue name. --- doc/release/monthly.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 514d73517b2ad..7c21c5f19a151 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -15,6 +15,8 @@ The RC1 release comes with the task to update the installation and upgrade docs. ### **1. Create an issue for RC1 release** +Consider naming the issue "Release x.x.x.rc1" to make it easier for later searches. + ### **2. Update the installation guide** 1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay) From 971dc47907ac54c49a5792697fc02ba350247f86 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 19 May 2014 12:43:16 +0200 Subject: [PATCH 02/68] Fix typo. --- doc/release/monthly.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 7c21c5f19a151..6c788e850eab6 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -84,7 +84,7 @@ Make sure the code quality indicators are green / good. ### **5. Set VERSION** -Set VERSION tot x.x.0.rc1 +Change version in VERSION to x.x.0.rc1 ### **6. Tag** From 0660183cb0733e68520e109c9470d9b4f4109194 Mon Sep 17 00:00:00 2001 From: dosire Date: Tue, 20 May 2014 13:29:52 +0200 Subject: [PATCH 03/68] Renamed based on suggestion Martijn. --- PROCESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PROCESS.md b/PROCESS.md index 23dbfd0c699b6..89ad12ecd5a59 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -13,7 +13,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co - Monitors all issues for feedback (but especially ones commented on since automatically watching them) - Closes issues with no feedback from the reporter for two weeks -### Merge request officers +### Merge marshal - Responds to merge requests the issue team mentions them in and monitors for new merge requests - Provides feedback to the merge request submitter to improve the merge request (style, tests, etc.) From 114e67564dbcfadafc237459a7549e5a85640817 Mon Sep 17 00:00:00 2001 From: dosire Date: Thu, 22 May 2014 14:40:38 +0200 Subject: [PATCH 04/68] The 4.0 raketask is no longer needed and rebuilding of missing satellites is automatic. --- doc/raketasks/README.md | 1 - doc/raketasks/features.md | 36 ------------------------------------ 2 files changed, 37 deletions(-) delete mode 100644 doc/raketasks/features.md diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 6be24f0102aeb..b1c97c8223703 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -1,6 +1,5 @@ + [Backup restore](backup_restore.md) + [Cleanup](cleanup.md) -+ [Features](features.md) + [Maintenance](maintenance.md) and self-checks + [User management](user_management.md) + [Web hooks](web_hooks.md) diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md deleted file mode 100644 index 018817d219ba7..0000000000000 --- a/doc/raketasks/features.md +++ /dev/null @@ -1,36 +0,0 @@ -### Enable usernames and namespaces for user projects - -This command will enable the namespaces feature introduced in v4.0. It will move every project in its namespace folder. - -Note: - -* Because the **repository location will change**, you will need to **update all your git url's** to point to the new location. -* Username can be changed at [Profile / Account](/profile/account) - -**Example:** - -Old path: `git@example.org:myrepo.git` -New path: `git@example.org:username/myrepo.git` or `git@example.org:groupname/myrepo.git` - -``` -bundle exec rake gitlab:enable_namespaces RAILS_ENV=production -``` - - -### Rebuild project satellites - -This command will build missing satellites for projects. After this you will be able to **merge a merge request** via GitLab and use the **online editor**. - -``` -bundle exec rake gitlab:satellites:create RAILS_ENV=production -``` - -Example output: - -``` -Creating satellite for abcd.git -[git clone output] -Creating satellite for abcd2.git -[git clone output] -done -``` From 0957f341b173c95c48906d7545531042cfd39641 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 22 May 2014 15:18:28 +0200 Subject: [PATCH 05/68] Updates for the monthly release document. --- doc/release/monthly.md | 69 +++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 6c788e850eab6..66b3bfcfd6dc9 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -100,12 +100,34 @@ Tweet about the RC release: > GitLab x.x.x.rc1 is out. This is a release candidate intended for testing only. Please let us know if you find regressions. -### **8. Update Cloud** +### **8. Update gitlab.com** -Merge the RC1 code into Cloud. Once the build is green, deploy in the morning. +Merge the RC1 code into gitlab.com. Once the build is green, deploy in the morning. It is important to do this as soon as possible, so we can catch any errors before we release the full version. +# **21st - Preparation ** + +### **1. Prepare the blog post** + +* Check the changelog of CE and EE for important changes. Based on [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) fill in the important information. +* Create a WIP MR for the blog post and cc the team so everyone can give feedback. +* Ask Dmitriy to add screenshots to the WIP MR. +* Decide with team who will be the MVP user. +* Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. + +### **2. Q&A** + +Create issue on dev.gitlab.org gitlab repository, named "GitLab X.X release" in order to keep track of the progress. + +Use the omnibus packages or cookbook to test using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md). + +**NOTE** Upgrader can only be tested when tags are pushed to all repositories. Do not forget to confirm it is working before releasing. Note that in the issue. + + +### **3. Fix anything coming out of the QA** + +Create an issue with description of a problem, if it is quick fix fix yourself otherwise contact the team for advice. # **22nd - Release CE and EE** @@ -127,27 +149,31 @@ git push x-x-stable ### **2. Build the Omnibus packages** [Follow this guide](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md) -### **3. QA** -Use the omnibus packages to test using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md) +### **3. Set VERSION to x.x.x and push** +Change the VERSION file in `master` branch of the CE repository and commit. +Cherry-pick into the `x-x-stable` branch of CE. -### **4. Fix anything coming out of the QA** +Change the VERSION file in `master branch of the EE repository and commit. +Cherry-pick into the `x-x-stable-ee` branch of EE. -### **5. Set VERSION to x.x.0** +### **4. Create annotated tag vx.x.x** + +In `x-x-stable` branch check for the sha1 of the commit with VERSION file changed. Tag that commit, -### **6. Create annotated tag vx.x.0** ``` -git tag -a vx.x.0 -m 'Version x.x.0' +git tag -a vx.x.0 -m 'Version x.x.0' xxxxx ``` -### **7. Push VERSION + Tag to master, merge into x-x-stable** +where `xxxxx` is sha1. + +### **5. Push the tag** + ``` -git push origin master +git push origin vx.x.0 ``` -Next, merge the VERSION into the x-x-stable branch. - -### **8. Push to remotes** +### **6. Push to remotes** For GitLab CE, push to dev, GitLab.com and GitHub. @@ -155,15 +181,22 @@ For GitLab EE, push to the subscribers repo. NOTE: You might not have the rights to push to master on dev. Ask Dmitriy. -### **9. Publish blog for new release** -* Mention what GitLab is on the second line: GitLab is open source software to collaborate on code. -* Select and thank the the Most Valuable Person (MVP) of this release. -* Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. +### **7. Publish blog for new release** -### **10. Tweet to blog** +Merge the [blog merge request](#1-prepare-the-blog-post) in `www-gitlab-com` repository. + +### **8. Tweet to blog** Send out a tweet to share the good news with the world. List the features in short and link to the blog post. +Proposed tweet for CE "GitLab X.X.X CE is released! It brings *** " + +Proposed tweet for EE "GitLab X.X.X EE is released! It brings *** " + # **23rd - Optional Patch Release** +# **24th - Update GitLab.com** + +Merge the stable release into gitlab.com. Once the build is green deploy the next morning. + # **25th - Release GitLab CI** From b6a88c78500d66aa2965259e7db2574b1818602c Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 22 May 2014 15:41:33 +0200 Subject: [PATCH 06/68] Update casing of gitlab.com. --- doc/release/monthly.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 66b3bfcfd6dc9..f0c1b8b3e806d 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -100,9 +100,9 @@ Tweet about the RC release: > GitLab x.x.x.rc1 is out. This is a release candidate intended for testing only. Please let us know if you find regressions. -### **8. Update gitlab.com** +### **8. Update GitLab.com** -Merge the RC1 code into gitlab.com. Once the build is green, deploy in the morning. +Merge the RC1 code into GitLab.com. Once the build is green, deploy in the morning. It is important to do this as soon as possible, so we can catch any errors before we release the full version. @@ -197,6 +197,6 @@ Proposed tweet for EE "GitLab X.X.X EE is released! It brings *** Date: Mon, 19 May 2014 11:50:18 +0200 Subject: [PATCH 07/68] add newsletter to release doc Conflicts: doc/release/monthly.md --- doc/release/monthly.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index f0c1b8b3e806d..40cf47af64cd9 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -193,6 +193,12 @@ Proposed tweet for CE "GitLab X.X.X CE is released! It brings *** " +### **9. Send out newsletter** + +In mailchimp replicate the former release newsletters to customers / newsletter subscribers (these are two separate things) and modify them accordingly. + +Include a link to the blog post and keep it short. + # **23rd - Optional Patch Release** # **24th - Update GitLab.com** From ab126c806884f26a42d515b573ce202c49dfafcb Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 22 May 2014 17:20:50 +0200 Subject: [PATCH 08/68] Add proposed email for mailchimp. --- doc/release/monthly.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 40cf47af64cd9..ed580f666ac05 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -199,6 +199,8 @@ In mailchimp replicate the former release newsletters to customers / newsletter Include a link to the blog post and keep it short. +Proposed email for CE: "We have released a new version of GitLab Community Edition and its packages. See our blog post() for more information." + # **23rd - Optional Patch Release** # **24th - Update GitLab.com** From 6a85cdf1627629ecaa762fa60a7abdbd092cc20a Mon Sep 17 00:00:00 2001 From: Earle Bunao & Neil Calabroso Date: Fri, 23 May 2014 16:22:00 +0800 Subject: [PATCH 09/68] Implements drag and drop upload in creating issues --- Gemfile | 3 + Gemfile.lock | 3 + app/assets/javascripts/application.js.coffee | 1 + .../behaviors/toggler_behavior.coffee | 2 +- .../javascripts/markdown_area.js.coffee | 85 ++++++++++++++++++ app/assets/stylesheets/application.scss | 1 + app/assets/stylesheets/behaviors.scss | 13 ++- .../stylesheets/generic/markdown_area.scss | 58 ++++++++++++ app/assets/stylesheets/sections/notes.scss | 32 ++++--- app/controllers/files_controller.rb | 1 - app/controllers/projects/issues_controller.rb | 4 +- app/controllers/projects_controller.rb | 20 +++++ app/models/issue.rb | 4 + app/uploaders/file_uploader.rb | 41 +++++++++ app/views/layouts/admin.html.haml | 1 + app/views/layouts/projects.html.haml | 1 + app/views/projects/issues/_form.html.haml | 14 +-- app/views/projects/issues/show.html.haml | 2 +- .../projects/merge_requests/_form.html.haml | 10 ++- .../merge_requests/_new_submit.html.haml | 10 ++- app/views/projects/milestones/_form.html.haml | 10 ++- app/views/projects/notes/_form.html.haml | 25 +++--- app/views/projects/wikis/_form.html.haml | 13 ++- config/routes.rb | 3 +- spec/controllers/commits_controller_spec.rb | 3 +- spec/controllers/projects_controller_spec.rb | 44 +++++++++ spec/factories.rb | 2 +- spec/fixtures/banana_sample.gif | Bin 0 -> 71759 bytes spec/fixtures/doc_sample.txt | 3 + spec/fixtures/rails_sample.jpg | Bin 0 -> 35255 bytes 30 files changed, 363 insertions(+), 46 deletions(-) create mode 100644 app/assets/javascripts/markdown_area.js.coffee create mode 100644 app/assets/stylesheets/generic/markdown_area.scss create mode 100644 app/uploaders/file_uploader.rb create mode 100644 spec/controllers/projects_controller_spec.rb create mode 100644 spec/fixtures/banana_sample.gif create mode 100644 spec/fixtures/doc_sample.txt create mode 100644 spec/fixtures/rails_sample.jpg diff --git a/Gemfile b/Gemfile index f7e3fe7b6dd14..9775334a7c1ea 100644 --- a/Gemfile +++ b/Gemfile @@ -69,6 +69,9 @@ gem "haml-rails" # Files attachments gem "carrierwave" +# Drag and Drop UI +gem 'dropzonejs-rails' + # for aws storage gem "fog", "~> 1.14", group: :aws gem "unf", group: :aws diff --git a/Gemfile.lock b/Gemfile.lock index 86c752505bdb6..3d4a673af1ca5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,6 +103,8 @@ GEM diffy (3.0.3) docile (1.1.1) dotenv (0.9.0) + dropzonejs-rails (0.4.14) + rails (> 3.1) email_spec (1.5.0) launchy (~> 2.1) mail (~> 2.2) @@ -579,6 +581,7 @@ DEPENDENCIES devise (= 3.0.4) devise-async (= 0.8.0) diffy (~> 3.0.3) + dropzonejs-rails email_spec email_validator (~> 1.4.0) enumerize diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 587e51a7a8388..35e435546611b 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -29,6 +29,7 @@ #= require underscore #= require nprogress #= require nprogress-turbolinks +#= require dropzone #= require_tree . window.slugify = (text) -> diff --git a/app/assets/javascripts/behaviors/toggler_behavior.coffee b/app/assets/javascripts/behaviors/toggler_behavior.coffee index d06cb116dfed8..8ac5bfe95d874 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.coffee +++ b/app/assets/javascripts/behaviors/toggler_behavior.coffee @@ -1,6 +1,6 @@ $ -> $("body").on "click", ".js-toggler-target", -> - container = $(@).closest(".js-toggler-container") + container = $(".notes-container") container.toggleClass("on") # Toggle button. Show/hide content inside parent container. diff --git a/app/assets/javascripts/markdown_area.js.coffee b/app/assets/javascripts/markdown_area.js.coffee new file mode 100644 index 0000000000000..def5d12a820f9 --- /dev/null +++ b/app/assets/javascripts/markdown_area.js.coffee @@ -0,0 +1,85 @@ +formatLink = (str) -> + "![" + str.alt + "](" + str.url + ")" + +$(document).ready -> + alertClass = "alert alert-danger alert-dismissable div-dropzone-alert" + alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"" + divHover = "
" + divSpinner = "
" + divAlert = "
" + iconPicture = "" + iconSpinner = "" + btnAlert = "" + project_image_path_upload = window.project_image_path_upload or null + + $("textarea.markdown-area").wrap "
" + + $(".div-dropzone").parent().addClass "div-dropzone-wrapper" + + $(".div-dropzone").append divHover + $(".div-dropzone-hover").append iconPicture + $(".div-dropzone").append divSpinner + $(".div-dropzone-spinner").append iconSpinner + + + dropzone = $(".div-dropzone").dropzone( + url: project_image_path_upload + dictDefaultMessage: "" + clickable: true + paramName: "markdown_img" + maxFilesize: 10 + uploadMultiple: false + acceptedFiles: "image/jpg,image/jpeg,image/gif,image/png" + headers: + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + + previewContainer: false + + processing: -> + $(".div-dropzone-alert").alert "close" + + dragover: -> + $(".div-dropzone > textarea").addClass "div-dropzone-focus" + $(".div-dropzone-hover").css "opacity", 0.7 + return + + dragleave: -> + $(".div-dropzone > textarea").removeClass "div-dropzone-focus" + $(".div-dropzone-hover").css "opacity", 0 + return + + drop: -> + $(".div-dropzone > textarea").removeClass "div-dropzone-focus" + $(".div-dropzone-hover").css "opacity", 0 + $(".div-dropzone > textarea").focus() + return + + success: (header, response) -> + child = $(dropzone[0]).children("textarea") + $(child).val $(child).val() + formatLink(response.link) + "\n" + return + + error: (temp, errorMessage) -> + checkIfMsgExists = $(".error-alert").children().length + if checkIfMsgExists is 0 + $(".error-alert").append divAlert + $(".div-dropzone-alert").append btnAlert + errorMessage + return + + sending: -> + $(".div-dropzone-spinner").css "opacity", 0.7 + return + + complete: -> + $(".dz-preview").remove() + $(".markdown-area").trigger "input" + $(".div-dropzone-spinner").css "opacity", 0 + return + ) + + $(".markdown-selector").click (e) -> + e.preventDefault() + $(".div-dropzone").click() + return + + return \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index c53873f95a2af..0b372a87a111c 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,6 +10,7 @@ *= require_self *= require nprogress *= require nprogress-bootstrap + *= require dropzone/basic */ @import "main/*"; diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss index 3fdc20485f278..64e3c8d9ace2b 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -5,10 +5,19 @@ .js-details-container.open .content { display: block; } .js-details-container.open .content.hide { display: none; } - // Toggler //-------- -.js-toggler-container .turn-on { display: inherit; } +.write-preview-btn .turn-on { display: inherit; } +.write-preview-btn .turn-off { display: none; } + .js-toggler-container .turn-off { display: none; } .js-toggler-container.on .turn-on { display: none; } .js-toggler-container.on .turn-off { display: inherit; } + +.js-toggler-container.on ~ .note-form-actions { + .write-preview-btn .turn-on { display: none; } +} + +.js-toggler-container.on ~ .note-form-actions { + .write-preview-btn .turn-off { display: inherit; } +} diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss new file mode 100644 index 0000000000000..a1fe18b02fa38 --- /dev/null +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -0,0 +1,58 @@ +.div-dropzone-wrapper { + .div-dropzone { + position: relative; + padding: 0; + border: 0; + margin-bottom: 5px; + + .div-dropzone-focus { + border-color: #66afe9 !important; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important; + outline: 0 !important; + } + + .div-dropzone-hover { + position: absolute; + top: 50%; + left: 50%; + margin-top: -0.5em; + margin-left: -0.6em; + opacity: 0; + font-size: 50px; + transition: opacity 200ms ease-in-out; + } + + .div-dropzone-spinner { + position: absolute; + top: 100%; + left: 100%; + margin-top: -1.1em; + margin-left: -1.1em; + opacity: 0; + font-size: 30px; + transition: opacity 200ms ease-in-out; + } + + .div-dropzone-icon { + display: block; + text-align: center; + font-size: inherit; + } + + .dz-preview { + display: none; + } + } + + .hint { + float: left; + padding: 0; + margin: 0; + } +} + +.div-dropzone-alert { + margin-top: 5px; + margin-bottom: 0; + transition: opacity 200ms ease-in-out; +} diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index 7e56781f56a87..755295be1f403 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -272,21 +272,16 @@ ul.notes { margin-bottom: 0; } .note_text_and_preview { - // makes the "absolute" position for links relative to this - position: relative; - - // preview/edit buttons - > a { - position: absolute; - right: 5px; - bottom: -60px; - } .note_preview { background: #f5f5f5; border: 1px solid #ddd; @include border-radius(4px); min-height: 80px; padding: 4px 6px; + + > p { + overflow-x: auto; + } } .note_text { border: 1px solid #DDD; @@ -310,7 +305,6 @@ ul.notes { float: none; } - .common-note-form { margin: 0; background: #F9F9F9; @@ -318,7 +312,6 @@ ul.notes { border: 1px solid #DDD; } - .note-form-actions { background: #F9F9F9; height: 45px; @@ -333,6 +326,18 @@ ul.notes { .js-notify-commit-author { float: left; } + + .write-preview-btn { + // makes the "absolute" position for links relative to this + position: relative; + + // preview/edit buttons + > a { + position: absolute; + right: 5px; + top: 8px; + } + } } .note-edit-form { @@ -367,3 +372,8 @@ ul.notes { .parallel-comment { padding: 6px; } + +.error-alert > .alert { + margin-top: 5px; + margin-bottom: 5px; +} diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index bf30de565ed09..7937454810d99 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -14,4 +14,3 @@ def download end end end - diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4e7a716bfe411..6eec2094f86bb 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -69,7 +69,9 @@ def create render :new end end - format.js + format.js do |format| + @link = @issue.attachment.url.to_js + end end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e356d09a270e6..c15205fb68fb6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -162,8 +162,28 @@ def unarchive end end + def upload_image + uploader = FileUploader.new('uploads', upload_path, accepted_images) + alt = params['markdown_img'].original_filename + uploader.store!(params['markdown_img']) + link = { 'alt' => File.basename(alt, '.*'), + 'url' => File.join(root_url, uploader.url) } + respond_to do |format| + format.json { render json: { link: link } } + end + end + private + def upload_path + base_dir = FileUploader.generate_dir + File.join(repository.path_with_namespace, base_dir) + end + + def accepted_images + %w(png jpg jpeg gif) + end + def set_title @title = 'New Project' end diff --git a/app/models/issue.rb b/app/models/issue.rb index 16d51345e5a16..f0c2e5522738b 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -15,8 +15,12 @@ # milestone_id :integer # state :string(255) # iid :integer +# attachment :string(255) # +require 'carrierwave/orm/activerecord' +require 'file_size_validator' + class Issue < ActiveRecord::Base include Issuable include InternalId diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb new file mode 100644 index 0000000000000..cbc9271ac14c8 --- /dev/null +++ b/app/uploaders/file_uploader.rb @@ -0,0 +1,41 @@ +# encoding: utf-8 +class FileUploader < CarrierWave::Uploader::Base + storage :file + + def initialize(base_dir, path = '', allowed_extensions = nil) + @base_dir = base_dir + @path = path + @allowed_extensions = allowed_extensions + end + + def base_dir + @base_dir + end + + def store_dir + File.join(@base_dir, @path) + end + + def cache_dir + File.join(@base_dir, 'tmp', @path) + end + + def extension_white_list + @allowed_extensions + end + + def store!(file) + file.original_filename = self.class.generate_filename(file) + super + end + + def self.generate_filename(file) + original_filename = File.basename(file.original_filename, '.*') + extension = File.extname(file.original_filename) + new_filename = Digest::MD5.hexdigest(original_filename) + extension + end + + def self.generate_dir + SecureRandom.hex(5) + end +end diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 53e0dbaef9ba4..c7a827555a7a4 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -10,3 +10,4 @@ .container .content= yield + = yield :embedded_scripts \ No newline at end of file diff --git a/app/views/layouts/projects.html.haml b/app/views/layouts/projects.html.haml index 3ae4961b13752..11c815c52a764 100644 --- a/app/views/layouts/projects.html.haml +++ b/app/views/layouts/projects.html.haml @@ -14,3 +14,4 @@ .container .content= yield + = yield :embedded_scripts \ No newline at end of file diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 84703229fe6be..49d1a87743f13 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -5,6 +5,7 @@ - contribution_guide_url = project_blob_path(@project, tree_join(@repository.root_ref, @repository.contribution_guide.name)) .alert.alert-info.col-sm-10.col-sm-offset-2 ="Please review the #{link_to "guidelines for contribution", contribution_guide_url} to this repository.".html_safe + = form_for [@project, @issue], html: { class: 'form-horizontal issue-form' } do |f| -if @issue.errors.any? .alert.alert-danger @@ -19,8 +20,12 @@ .form-group = f.label :description, 'Description', class: 'control-label' .col-sm-10 - = f.text_area :description, class: "form-control js-gfm-input", rows: 14 - %p.hint Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + = f.text_area :description, class: 'form-control js-gfm-input markdown-area', rows: 14 + .col-sm-12.hint + .pull-left Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .clearfix + .error-alert %hr .form-group .issue-assignee @@ -57,9 +62,6 @@ - cancel_path = @issue.new_record? ? project_issues_path(@project) : project_issue_path(@project, @issue) = link_to "Cancel", cancel_path, class: 'btn btn-cancel' - - - :javascript $("#issue_label_list") .bind( "keydown", function( event ) { @@ -94,3 +96,5 @@ $('#issue_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); + + window.project_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index b6d3a8edf4da9..2c816e788decc 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -73,4 +73,4 @@ = label.name   -.voting_notes#notes= render "projects/notes/notes_with_form" +.voting_notes#notes= render "projects/notes/notes_with_form" \ No newline at end of file diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 290a15e2664d5..3088f0cf864a3 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -22,8 +22,12 @@ .form-group = f.label :description, "Description", class: 'control-label' .col-sm-10 - = f.text_area :description, class: "form-control js-gfm-input", rows: 14 - %p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + = f.text_area :description, class: "form-control js-gfm-input markdown-area", rows: 14 + .col-sm-12.hint + .pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .clearfix + .error-alert %hr .form-group .issue-assignee @@ -98,3 +102,5 @@ return false; } }); + + window.project_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index b5479be708b4d..dc41ef8135e08 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -23,8 +23,12 @@ .form-group .light = f.label :description, "Description" - = f.text_area :description, class: "form-control js-gfm-input", rows: 10 - %p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + = f.text_area :description, class: "form-control js-gfm-input markdown-area", rows: 10 + .col-sm-12.hint + .pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .clearfix + .error-alert .form-group .issue-assignee = f.label :assignee_id do @@ -80,3 +84,5 @@ $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); + + window.project_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index d770bb5b3710b..7e0fd19d0abfd 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -21,8 +21,12 @@ .form-group = f.label :description, "Description", class: "control-label" .col-sm-10 - = f.text_area :description, maxlength: 2000, class: "form-control", rows: 10 - %p.hint Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + = f.text_area :description, maxlength: 2000, class: "form-control markdown-area", rows: 10 + .hint + .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .pull-left Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .clearfix + .error-alert .col-md-6 .form-group = f.label :due_date, "Due Date", class: "control-label" @@ -45,3 +49,5 @@ 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_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 3db551e993b07..cadae9da8050e 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -5,20 +5,15 @@ = f.hidden_field :noteable_id = f.hidden_field :noteable_type - .note_text_and_preview.js-toggler-container - %a.btn.js-note-preview-button.js-toggler-target.turn-off{ href: "javascript:;", data: {url: preview_project_notes_path(@project)} } - %i.icon-eye-open - Preview - %a.btn.btn-primary.js-note-edit-button.js-toggler-target.turn-off{ href: "javascript:;" } - %i.icon-edit - Write - - = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on' + .note_text_and_preview.js-toggler-container.notes-container + = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on markdown-area' .note_preview.js-note-preview.turn-off .hint - .pull-right Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. .clearfix + .error-alert .note-form-actions .buttons @@ -35,4 +30,14 @@ %span.file_name.js-attachment-filename File name... = f.file_field :attachment, class: "js-note-attachment-input hidden" + .write-preview-btn + %a.btn.js-note-preview-button.js-toggler-target.turn-off{ href: "javascript:;", data: {url: preview_project_notes_path(@project)} } + %i.icon-eye-open + Preview + %a.btn.btn-primary.js-note-edit-button.js-toggler-target.turn-off{ href: "javascript:;" } + %i.icon-edit + Write .clearfix + +:javascript + window.project_image_path_upload = "#{upload_image_project_path @project}"; diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 0c2e33f22820c..a43e6f073e155 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -15,7 +15,6 @@ .col-sm-2 .col-sm-10 %p.cgray - Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. To link to a (new) page you can just type %code [Link Title](page-slug) \. @@ -23,8 +22,12 @@ .form-group = f.label :content, class: 'control-label' .col-sm-10 - = f.text_area :content, class: 'form-control js-gfm-input', rows: 18 - + = f.text_area :content, class: 'form-control js-gfm-input markdown-area', rows: 18 + .col-sm-12.hint + .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .clearfix + .error-alert .form-group = f.label :commit_message, class: 'control-label' .col-sm-10= f.text_field :message, class: 'form-control', rows: 18 @@ -36,3 +39,7 @@ - else = f.submit 'Create page', class: "btn-create btn" = link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel" + +:javascript + window.project_image_path_upload = "#{upload_image_project_path @project}"; + diff --git a/config/routes.rb b/config/routes.rb index 7641fe430889b..da5a1ba7a871c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -136,8 +136,6 @@ match "/u/:username" => "users#show", as: :user, constraints: { username: /.*/ }, via: :get - - # # Dashboard Area # @@ -178,6 +176,7 @@ post :fork post :archive post :unarchive + post :upload_image get :autocomplete_sources get :import put :retry_import diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/commits_controller_spec.rb index fbf4f29acfd69..308cfa692192e 100644 --- a/spec/controllers/commits_controller_spec.rb +++ b/spec/controllers/commits_controller_spec.rb @@ -6,8 +6,7 @@ before do sign_in(user) - - project.team << [user, :master] + project.creator = user end describe "GET show" do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb new file mode 100644 index 0000000000000..07ca8d2502665 --- /dev/null +++ b/spec/controllers/projects_controller_spec.rb @@ -0,0 +1,44 @@ +require('spec_helper') + +describe ProjectsController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:png) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') } + let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:gif) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + + describe "POST #upload_image" do + before do + sign_in(user) + end + + context "without params['markdown_img']" do + it "returns an error" do + post :upload_image, id: project.to_param + expect(response.status).to eq(404) + end + end + + context "with invalid file" do + before do + post :upload_image, id: project.to_param, markdown_img: @img + end + + it "returns an error" do + expect(response.status).to eq(404) + end + end + + context "with valid file" do + before do + post :upload_image, id: project.to_param, markdown_img: @img + end + + it "returns a content with original filename and new link." do + link = { alt: 'rails_sample', link: '' }.to_json + expect(response.body).to have_content link + end + end + end +end \ No newline at end of file diff --git a/spec/factories.rb b/spec/factories.rb index 148477d638901..41cc99cbcb91e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -201,7 +201,7 @@ end trait :with_attachment do - attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") } + attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end end diff --git a/spec/fixtures/banana_sample.gif b/spec/fixtures/banana_sample.gif new file mode 100644 index 0000000000000000000000000000000000000000..1322ac92d141f2240f3b68b1c4a10b5e18b49e2b GIT binary patch literal 71759 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 B?fU=# literal 0 HcmV?d00001 From 8ec38c17e2dfa103bfbcd50ef5481e6e2c1fd4ef Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 May 2014 13:19:39 +0300 Subject: [PATCH 10/68] Mention changes to protected branches Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ca0b96b840223..484d66daa5ea1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,8 @@ v 7.0.0 - The CPU no longer overheats when you hold down the spacebar - Improve edit file UI - Add ability to upload group avatar when create + - Protected branch cannot be removed + - Developers can remove normal branches with UI v 6.9.0 - Store Rails cache data in the Redis `cache:gitlab` namespace From 8ae2d2152b197931725a3f4b9aa3697f37988c20 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 May 2014 13:22:10 +0300 Subject: [PATCH 11/68] Fix branch protection help doc Signed-off-by: Dmitriy Zaporozhets --- doc/permissions/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 9be6423f66761..9a3dcefe5698f 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -24,7 +24,7 @@ If a user is a GitLab administrator they receive all permissions. |Manage issue tracker| ||✓|✓|✓| |Add new team members| |||✓|✓| |Push to protected branches| |||✓|✓| -|Remove protected branches| |||✓|✓| +|Enable/Disable branch protection| |||✓|✓| |Edit project| |||✓|✓| |Add Deploy Keys to project| |||✓|✓| |Configure Project Hooks| |||✓|✓| From a5e1624359d9960782cd76f24c6300aeb28a2746 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 May 2014 13:27:35 +0300 Subject: [PATCH 12/68] Redirect to tree view after branch created Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/branches_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 00811f17adb17..e4c82d019800e 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -15,9 +15,9 @@ def recent end def create - CreateBranchService.new.execute(project, params[:branch_name], params[:ref], current_user) + @branch = CreateBranchService.new.execute(project, params[:branch_name], params[:ref], current_user) - redirect_to project_branches_path(@project) + redirect_to project_tree_path(@project, @branch.name) end def destroy From b44138ae4030bb5b63af4f4d835ddceb06c1b953 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 May 2014 13:29:14 +0300 Subject: [PATCH 13/68] Branch link on branches page goes to tree view instead of commits Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/branches/_branch.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 87f4dd88c27e0..d5f7dacaa05a7 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,7 +1,7 @@ - commit = @repository.commit(branch.target) %li %h4 - = link_to project_commits_path(@project, branch.name) do + = link_to project_tree_path(@project, branch.name) do %strong= truncate(branch.name, length: 60) - if branch.name == @repository.root_ref %span.label.label-info default From 695fd3cf2c7e01d06e9c335cae208089a4c6bfbd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 May 2014 14:25:55 +0300 Subject: [PATCH 14/68] Move protected branches to Project settings Signed-off-by: Dmitriy Zaporozhets --- .../projects/branches_controller.rb | 4 +- .../projects/protected_branches_controller.rb | 4 +- app/helpers/tab_helper.rb | 2 +- app/models/repository.rb | 21 ++-- app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/_settings_nav.html.haml | 4 + app/views/projects/branches/_branch.html.haml | 10 +- app/views/projects/branches/_filter.html.haml | 27 ------ app/views/projects/branches/index.html.haml | 39 ++++++-- app/views/projects/branches/recent.html.haml | 8 -- app/views/projects/commits/_head.html.haml | 4 +- .../protected_branches/index.html.haml | 96 +++++++++---------- config/routes.rb | 7 +- 13 files changed, 109 insertions(+), 119 deletions(-) delete mode 100644 app/views/projects/branches/_filter.html.haml delete mode 100644 app/views/projects/branches/recent.html.haml diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 00811f17adb17..8c70ae45f06f7 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -7,7 +7,9 @@ class Projects::BranchesController < Projects::ApplicationController before_filter :authorize_push!, only: [:create, :destroy] def index - @branches = Kaminari.paginate_array(@repository.branches).page(params[:page]).per(30) + @sort = params[:sort] || 'name' + @branches = @repository.branches_sorted_by(@sort) + @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(30) end def recent diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index cfc1bd99a2040..e39e97af8dd1c 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,9 +1,9 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController # Authorize - before_filter :authorize_read_project! before_filter :require_non_empty_project + before_filter :authorize_admin_project! - before_filter :authorize_admin_project!, only: [:destroy, :create] + layout "project_settings" def index @branches = @project.protected_branches.to_a diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index bd373c5f3cfa6..610175f844791 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -75,7 +75,7 @@ def nav_link(options = {}, &block) def project_tab_class return "active" if current_page?(controller: "/projects", action: :edit, id: @project) - if ['services', 'hooks', 'deploy_keys', 'team_members'].include? controller.controller_name + if ['services', 'hooks', 'deploy_keys', 'team_members', 'protected_branches'].include? controller.controller_name "active" end end diff --git a/app/models/repository.rb b/app/models/repository.rb index eadc34127c2bd..b7239236aea1b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -55,12 +55,6 @@ def find_tag(name) tags.find { |tag| tag.name == name } end - def recent_branches(limit = 20) - branches.sort do |a, b| - commit(b.target).committed_date <=> commit(a.target).committed_date - end[0..limit] - end - def add_branch(branch_name, ref) Rails.cache.delete(cache_key(:branch_names)) @@ -220,4 +214,19 @@ def last_commit_for_path(sha, path) def clean_old_archives Gitlab::Popen.popen(%W(find #{Gitlab.config.gitlab.repository_downloads_path} -mmin +120 -delete)) end + + def branches_sorted_by(value) + case value + when 'recently_updated' + branches.sort do |a, b| + commit(b.target).committed_date <=> commit(a.target).committed_date + end + when 'last_updated' + branches.sort do |a, b| + commit(a.target).committed_date <=> commit(b.target).committed_date + end + else + branches + end + end end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 1f70cf1798721..d7d330e2a616e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -8,7 +8,7 @@ = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref) - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories protected_branches tags branches)) do + = nav_link(controller: %w(commit commits compare repositories tags branches)) do = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref) - if project_nav_tab? :network diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml index 4c7b088ae9a54..9b73f06a5be55 100644 --- a/app/views/projects/_settings_nav.html.haml +++ b/app/views/projects/_settings_nav.html.haml @@ -19,3 +19,7 @@ = link_to project_services_path(@project) do %i.icon-cogs Services + = nav_link(controller: :protected_branches) do + = link_to project_protected_branches_path(@project) do + %i.icon-lock + Protected branches diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 87f4dd88c27e0..a5f382c9a5cfd 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -8,6 +8,7 @@ - if @project.protected_branch? branch.name %span.label.label-success %i.icon-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-small' @@ -21,13 +22,8 @@ %i.icon-trash - if commit - %p - = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do - = commit.short_id - = image_tag avatar_icon(commit.author_email), class: "avatar s16", alt: '' - %span.light - = gfm escape_once(truncate(commit.title, length: 40)) - #{time_ago_with_tooltip(commit.committed_date)} + %ul.list-unstyled + = render 'projects/commits/inline_commit', commit: commit, project: @project - else %p Cant find HEAD commit for this branch diff --git a/app/views/projects/branches/_filter.html.haml b/app/views/projects/branches/_filter.html.haml deleted file mode 100644 index 7e1478292e01b..0000000000000 --- a/app/views/projects/branches/_filter.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -%ul.nav.nav-pills.nav-stacked - = nav_link(path: 'branches#recent') do - = link_to recent_project_branches_path(@project) do - Recent - .pull-right - = @repository.recent_branches.count - - = nav_link(path: 'protected_branches#index') do - = link_to project_protected_branches_path(@project) do - Protected - %i.icon-lock - .pull-right - = @project.protected_branches.count - - = nav_link(path: 'branches#index') do - = link_to project_branches_path(@project) do - All branches - .pull-right - = @repository.branch_names.count - - -%hr -- if can? current_user, :push_code, @project - = link_to new_project_branch_path(@project), class: 'btn btn-create' do - %i.icon-add-sign - New branch - diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index bee04eb013ebb..8bc4a8f7e989c 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,10 +1,31 @@ = render "projects/commits/head" -.row - .col-md-3 - = render "filter" - .col-md-9 - - unless @branches.empty? - %ul.bordered-list.top-list.all-branches - - @branches.each do |branch| - = render "projects/branches/branch", branch: branch - = paginate @branches, theme: 'gitlab' +%h3.page-title + Branches + .pull-right + - if can? current_user, :push_code, @project + = link_to new_project_branch_path(@project), class: 'btn btn-create' do + %i.icon-add-sign + New branch +   + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light sort: + - if @sort.present? + = @sort.humanize + - else + Name + %b.caret + %ul.dropdown-menu + %li + = link_to project_branches_path(sort: nil) do + Name + = link_to project_branches_path(sort: 'recently_updated') do + Recently updated + = link_to project_branches_path(sort: 'last_updated') do + Last updated +%hr +- unless @branches.empty? + %ul.bordered-list.top-list.all-branches + - @branches.each do |branch| + = render "projects/branches/branch", branch: branch + = paginate @branches, theme: 'gitlab' diff --git a/app/views/projects/branches/recent.html.haml b/app/views/projects/branches/recent.html.haml deleted file mode 100644 index 37d7919121e7d..0000000000000 --- a/app/views/projects/branches/recent.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -= render "projects/commits/head" -.row - .col-md-3 - = render "filter" - .col-md-9 - %ul.bordered-list.top-list - - @branches.each do |branch| - = render "projects/branches/branch", branch: branch diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 81e3374391150..0facfc4b5f1d5 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -7,9 +7,9 @@ = link_to 'Compare', project_compare_index_path(@project, from: @repository.root_ref, to: @ref || @repository.root_ref) = nav_link(html_options: {class: branches_tab_class}) do - = link_to recent_project_branches_path(@project) do + = link_to project_branches_path(@project) do Branches - %span.badge= @repository.branches.length + %span.badge= @repository.branches.size = nav_link(controller: :tags) do = link_to project_tags_path(@project) do diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index e9f67b671bf10..e51bf1d3a8cdc 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,52 +1,50 @@ -= render "projects/commits/head" -.row - .col-md-3 - = render "projects/branches/filter" - .col-md-9 - .bs-callout.bs-callout-info - %p Protected branches designed to - %ul - %li prevent push for all except #{link_to "masters", help_permissions_path, class: "vlink"}. - %li prevent branch from force push - %li prevent branch from removal - %p This ability allows to keep stable branches secured and force code review before merge to protected branches - %p Read more about project permissions #{link_to "here", help_permissions_path, class: "underlined-link"} +%h3.page-title Protected branches +%p.light This ability allows to keep stable branches secured and force code review before merge to protected branches +%hr - - if can? current_user, :admin_project, @project - = form_for [@project, @protected_branch], html: { class: 'form-horizontal' } do |f| - -if @protected_branch.errors.any? - .alert.alert-danger - %ul - - @protected_branch.errors.full_messages.each do |msg| - %li= msg +.bs-callout.bs-callout-info + %p Protected branches designed to + %ul + %li prevent push for all except #{link_to "masters", help_permissions_path, class: "vlink"}. + %li prevent branch from force push + %li prevent branch from removal + %p Read more about project permissions #{link_to "here", help_permissions_path, class: "underlined-link"} - .form-group - = f.label :name, "Branch", class: 'control-label' - .col-sm-10 - = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"}) - .form-actions - = f.submit 'Protect', class: "btn-create btn" - - unless @branches.empty? - %h5 Already Protected: - %ul.bordered-list.protected-branches-list - - @branches.each do |branch| - %li - %h4 - = link_to project_commits_path(@project, branch.name) do - %strong= branch.name - - if @project.root_ref?(branch.name) - %span.label.label-info default - %span.label.label-success - %i.icon-lock - .pull-right - - if can? current_user, :admin_project, @project - = link_to 'Unprotect', [@project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small" +- if can? current_user, :admin_project, @project + = form_for [@project, @protected_branch], html: { class: 'form-horizontal' } do |f| + -if @protected_branch.errors.any? + .alert.alert-danger + %ul + - @protected_branch.errors.full_messages.each do |msg| + %li= msg - - if commit = branch.commit - = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do - = commit.short_id - %span.light - = gfm escape_once(truncate(commit.title, length: 40)) - #{time_ago_with_tooltip(commit.committed_date)} - - else - (branch was removed from repository) + .form-group + = f.label :name, "Branch", class: 'control-label' + .col-sm-10 + = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"}) + .form-actions + = f.submit 'Protect', class: "btn-create btn" +- unless @branches.empty? + %h5 Already Protected: + %ul.bordered-list.protected-branches-list + - @branches.each do |branch| + %li + %h4 + = link_to project_commits_path(@project, branch.name) do + %strong= branch.name + - if @project.root_ref?(branch.name) + %span.label.label-info default + %span.label.label-success + %i.icon-lock + .pull-right + - if can? current_user, :admin_project, @project + = link_to 'Unprotect', [@project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small" + + - if commit = branch.commit + = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do + = commit.short_id + %span.light + = gfm escape_once(truncate(commit.title, length: 40)) + #{time_ago_with_tooltip(commit.committed_date)} + - else + (branch was removed from repository) diff --git a/config/routes.rb b/config/routes.rb index da5a1ba7a871c..ada9bb1d77a99 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -243,12 +243,7 @@ end end - resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do - collection do - get :recent, constraints: { id: Gitlab::Regex.git_reference_regex } - end - end - + resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :protected_branches, only: [:index, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } From 484c09792c5c6b4f8f1111978a82cf080a31a920 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 May 2014 14:33:20 +0300 Subject: [PATCH 15/68] Fix tests Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/tree/show.html.haml | 2 +- features/steps/project/browse_branches.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 6b33493b7d299..735ffae9e2cf2 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,6 +1,6 @@ %div.tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-small tree-ref-holder pull-right', split_button: true + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-small pull-right', split_button: true %div#tree-holder.tree-holder = render "tree", tree: @tree diff --git a/features/steps/project/browse_branches.rb b/features/steps/project/browse_branches.rb index 30c8cef80c836..5b5e8ba493e08 100644 --- a/features/steps/project/browse_branches.rb +++ b/features/steps/project/browse_branches.rb @@ -44,7 +44,7 @@ class ProjectBrowseBranches < Spinach::FeatureSteps end step 'I should see new branch created' do - within '.all-branches' do + within '.tree-ref-holder' do page.should have_content 'deploy_keys' end end From c5922ca97fa83492b0c8419b642be00d820ce0fb Mon Sep 17 00:00:00 2001 From: dosire Date: Fri, 23 May 2014 13:48:56 +0200 Subject: [PATCH 16/68] Don't forget to update the changelog. --- doc/release/monthly.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 514d73517b2ad..526d5d90e454a 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -9,6 +9,10 @@ NOTE: This is a guide for GitLab developers. A release manager is selected that coordinates the entire release of this version. 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. +### **3. Update Changelog** + +Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is asked if there is anything missing. + # **18th - Releasing RC1** The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub. From 8efc80ef0629bc5916debb4bc476374ac5ad6733 Mon Sep 17 00:00:00 2001 From: dosire Date: Fri, 23 May 2014 14:02:39 +0200 Subject: [PATCH 17/68] Add threaded emails to changelog. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 484d66daa5ea1..2cd68ce414f01 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 6.9.0 - Add more access checks during API calls - Block SSH access for 'disabled' Active Directory users - Labels for merge requests (Drew Blessing) + - Threaded emails by setting a Message-ID (Philip Blatter) v 6.8.0 - Ability to at mention users that are participating in issue and merge req. discussion From 6990f8a8464fd579c7152512d3d3b25b60b6f5c9 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 23 May 2014 14:07:42 +0200 Subject: [PATCH 18/68] Replace jquery deprecated .live with .on --- app/assets/javascripts/commits.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index 9c004c997edf6..784d7d20bb13d 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -12,7 +12,7 @@ class CommitsList $('.loading').hide() @init: (ref, limit) -> - $(".day-commits-table li.commit").live 'click', (event) -> + $("body").on "click", ".day-commits-table li.commit", (event) -> if event.target.nodeName != "A" location.href = $(this).attr("url") e.stopPropagation() From 7f84ba8662384cebf5574cbd2da5841aab2eac90 Mon Sep 17 00:00:00 2001 From: dosire Date: Fri, 23 May 2014 14:31:09 +0200 Subject: [PATCH 19/68] Project visibility setting is missed by people sometimes. --- doc/public_access/public_access.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index 76d83e6f3b69f..bb23a4bfd96fa 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -26,3 +26,7 @@ The public page of users, located at `/u/username` is visible if either: Otherwise, you will be redirected to the sign in page. When visiting the public page of an user, you will only see listed projects which you can view yourself. + +#### Restricting the use of public or internal projects +In [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/dbd88d453b8e6c78a423fa7e692004b1db6ea069/config/gitlab.yml.example#L64) you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. + From d575ee3e90a7b13e4ed29fdea0b611ae8ef50496 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 May 2014 15:34:02 +0300 Subject: [PATCH 20/68] Improve branch deletion via API Signed-off-by: Dmitriy Zaporozhets --- doc/api/branches.md | 14 ++++++++++++++ lib/api/branches.rb | 8 +++++++- spec/requests/api/branches_spec.rb | 23 ++++++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/doc/api/branches.md b/doc/api/branches.md index da9d4193f204e..bb2d3fec09d92 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -199,3 +199,17 @@ Parameters: "protected": false } ``` + +## Delete repository branch + + +``` +DELETE /projects/:id/repository/branches/:branch +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `branch` (required) - The name of the branch + +It return 200 if succeed or 405 if failed with error message explaining reason. diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 32597eb94c478..b32a4aa7bc286 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -94,7 +94,13 @@ class Branches < Grape::API # DELETE /projects/:id/repository/branches/:branch delete ":id/repository/branches/:branch" do authorize_push_project - DeleteBranchService.new.execute(user_project, params[:branch], current_user) + result = DeleteBranchService.new.execute(user_project, params[:branch], current_user) + + if result[:state] == :success + true + else + render_api_error!(result[:message], 405) + end end end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index abf6a8646ecbb..72589da5d4034 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -91,7 +91,6 @@ end end - describe "POST /projects/:id/repository/branches" do it "should create a new branch" do post api("/projects/#{project.id}/repository/branches", user), @@ -112,4 +111,26 @@ response.status.should == 403 end end + + describe "DELETE /projects/:id/repository/branches/:branch" do + before { Repository.any_instance.stub(rm_branch: true) } + + it "should remove branch" do + delete api("/projects/#{project.id}/repository/branches/new_design", user) + response.status.should == 200 + end + + it "should remove protected branch" do + project.protected_branches.create(name: 'new_design') + delete api("/projects/#{project.id}/repository/branches/new_design", user) + response.status.should == 405 + json_response['message'].should == 'Protected branch cant be removed' + end + + it "should not remove HEAD branch" do + delete api("/projects/#{project.id}/repository/branches/master", user) + response.status.should == 405 + json_response['message'].should == 'Cannot remove HEAD branch' + end + end end From 245c04c29a04906113991144d8d4eebc5c6ecf1c Mon Sep 17 00:00:00 2001 From: dosire Date: Fri, 23 May 2014 15:01:02 +0200 Subject: [PATCH 21/68] Change the license to B.V. --- CONTRIBUTING.md | 2 +- LICENSE | 2 +- .../corporate_contributor_license_agreement.md | 14 +++++++------- .../individual_contributor_license_agreement.md | 16 ++++++++-------- doc/workflow/project_features.md | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 780db547f829d..85f751919e9e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ If you can, please submit a merge request with the fix or improvements including 1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submittion 1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). -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.com 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 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. 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? The smaller a MR is the more likely it is it will be merged, after that you can send more MR's to enhance it. diff --git a/LICENSE b/LICENSE index 8ebd322ffc892..d11b8730bf124 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2014 Dmitriy Zaporozhets +Copyright (c) 2011-2014 GitLab B.V. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md index bbc274f3b0cc7..fb8d52e6bd41f 100644 --- a/doc/legal/corporate_contributor_license_agreement.md +++ b/doc/legal/corporate_contributor_license_agreement.md @@ -1,14 +1,14 @@ -You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab.com. Except for the license granted herein to GitLab.com and recipients of software distributed by GitLab.com, You reserve all right, title, and interest in and to Your Contributions. +You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions. 1. Definitions. - "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab.com. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab.com for inclusion in, or documentation of, any of the products owned or managed by GitLab.com (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab.com or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab.com for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + "Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." -2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab.com and to recipients of software distributed by GitLab.com a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. +2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. -3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab.com and to recipients of software distributed by GitLab.com a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation designated on Schedule A below (or in a subsequent written modification to that Schedule) is authorized to submit Contributions on behalf of the Corporation. @@ -16,9 +16,9 @@ You accept and agree to the following terms and conditions for Your present and 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. -7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab.com separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". +7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". -8. It is your responsibility to notify GitLab.com when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab.com. +8. It is your responsibility to notify GitLab B.V. when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V.. --------------------------------------- diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md index eaf5812ca4c5a..7ac9d6e4cde78 100644 --- a/doc/legal/individual_contributor_license_agreement.md +++ b/doc/legal/individual_contributor_license_agreement.md @@ -1,24 +1,24 @@ -You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab.com. Except for the license granted herein to GitLab.com and recipients of software distributed by GitLab.com, You reserve all right, title, and interest in and to Your Contributions. +You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions. 1. Definitions. - "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab.com. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab.com for inclusion in, or documentation of, any of the products owned or managed by GitLab.com (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab.com or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab.com for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." -2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab.com and to recipients of software distributed by GitLab.com a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. +2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. -3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab.com and to recipients of software distributed by GitLab.com a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. +3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. -4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to GitLab.com, or that your employer has executed a separate Corporate CLA with GitLab.com. +4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to GitLab B.V., or that your employer has executed a separate Corporate CLA with GitLab B.V.. 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. -7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab.com separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [[]named here]". +7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [[]named here]". -8. You agree to notify GitLab.com of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. +8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. --------------------------------------- diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md index 25fe403257083..64f4ddaa1255a 100644 --- a/doc/workflow/project_features.md +++ b/doc/workflow/project_features.md @@ -7,7 +7,7 @@ Below you will find a more elaborate explanation of each of these. Issues is a really powerful, but lightweight issue tracking system. You can make tickets, assign them to people, file them under milestones, order them with labels and have discussion in them. They integrate deeply into GitLab and are easily referenced from anywhere by using # and the issuenumber. -At GitLab.com, we use this for all our project management needs. + ## Merge Requests From 3e1853c5a0d89147a6a44a31a302f93e96c2ed92 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 May 2014 16:09:54 +0300 Subject: [PATCH 22/68] More stuff to CHANGELOG Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2cd68ce414f01..9d2ac06277b12 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,9 @@ v 7.0.0 - Add ability to upload group avatar when create - Protected branch cannot be removed - Developers can remove normal branches with UI + - Remove branch via API (sponsored by O'Reilly Media) + - Move protected branches page to Project settings area + - Redirect to Files view when create new branch via UI v 6.9.0 - Store Rails cache data in the Redis `cache:gitlab` namespace From cc28b21adce61206e5ca96dfe23bab4cec9a0400 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 May 2014 10:30:58 +0300 Subject: [PATCH 23/68] Add drag-n-drop to CHANGELOG Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 9d2ac06277b12..d265d3a0b41cd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 7.0.0 - Remove branch via API (sponsored by O'Reilly Media) - Move protected branches page to Project settings area - Redirect to Files view when create new branch via UI + - Drag and drop upload of image in every markdown-area (Earle Randolph Bunao and Neil Francis Calabroso) v 6.9.0 - Store Rails cache data in the Redis `cache:gitlab` namespace From 0853eebd1c1d80fdcb467a2cafede6f20e62759f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 May 2014 11:11:45 +0300 Subject: [PATCH 24/68] Improve comments css Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/sections/notes.scss | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index 755295be1f403..75cb8e5615236 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -83,6 +83,7 @@ ul.notes { overflow: hidden; display: block; position:relative; + border-bottom: 1px solid #eee; p { color: $style_color; } .avatar { @@ -98,22 +99,16 @@ ul.notes { .note-header { padding-bottom: 3px; } + + &:last-child { + border-bottom: none; + } } .note:target { -webkit-animation:target-note 2s linear; background: #fffff0; } - - // paint top or bottom borders depending on notes direction - &:not(.reversed) .note, - &:not(.reversed) .discussion { - border-bottom: 1px solid #eee; - } - &.reversed .note, - &.reversed .discussion { - border-top: 1px solid #eee; - } } .diff-file .notes_holder { @@ -308,14 +303,13 @@ ul.notes { .common-note-form { margin: 0; background: #F9F9F9; - padding: 3px; + padding: 5px; border: 1px solid #DDD; } .note-form-actions { background: #F9F9F9; height: 45px; - padding: 0 5px; .note-form-option { margin-top: 8px; From 5cfaf945cdad16baf877c873c26bc58b004707ae Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 May 2014 13:06:44 +0300 Subject: [PATCH 25/68] Improve notes form UI/UX Signed-off-by: Dmitriy Zaporozhets --- .../behaviors/toggler_behavior.coffee | 4 -- .../javascripts/merge_request.js.coffee | 12 ++--- app/assets/javascripts/notes.js.coffee | 30 +++++++++++-- .../stylesheets/generic/markdown_area.scss | 6 --- app/assets/stylesheets/sections/notes.scss | 45 ++++++++++--------- .../projects/merge_requests/_show.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 34 +++++++------- 7 files changed, 75 insertions(+), 58 deletions(-) diff --git a/app/assets/javascripts/behaviors/toggler_behavior.coffee b/app/assets/javascripts/behaviors/toggler_behavior.coffee index 8ac5bfe95d874..1b2ed9efc253b 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.coffee +++ b/app/assets/javascripts/behaviors/toggler_behavior.coffee @@ -1,8 +1,4 @@ $ -> - $("body").on "click", ".js-toggler-target", -> - container = $(".notes-container") - container.toggleClass("on") - # Toggle button. Show/hide content inside parent container. # Button does not change visibility. If button has icon - it changes chevron style. # diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 7589fc70cd833..b04a81c85eb5a 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -43,7 +43,7 @@ class MergeRequest , 'json' bindEvents: -> - this.$('.nav-tabs').on 'click', 'a', (event) => + this.$('.merge-request-tabs').on 'click', 'a', (event) => a = $(event.currentTarget) href = a.attr('href') @@ -51,7 +51,7 @@ class MergeRequest event.preventDefault() - this.$('.nav-tabs').on 'click', 'li', (event) => + this.$('.merge-request-tabs').on 'click', 'li', (event) => this.activateTab($(event.currentTarget).data('action')) this.$('.accept_merge_request').on 'click', -> @@ -71,15 +71,15 @@ class MergeRequest this.$('.remove_source_branch_widget.failed').show() activateTab: (action) -> - this.$('.nav-tabs li').removeClass 'active' + this.$('.merge-request-tabs li').removeClass 'active' this.$('.tab-content').hide() switch action when 'diffs' - this.$('.nav-tabs .diffs-tab').addClass 'active' + this.$('.merge-request-tabs .diffs-tab').addClass 'active' this.loadDiff() unless @diffs_loaded this.$('.diffs').show() else - this.$('.nav-tabs .notes-tab').addClass 'active' + this.$('.merge-request-tabs .notes-tab').addClass 'active' this.$('.notes').show() showState: (state) -> @@ -107,7 +107,7 @@ class MergeRequest loadDiff: (event) -> $.ajax type: 'GET' - url: this.$('.nav-tabs .diffs-tab a').attr('href') + url: this.$('.merge-request-tabs .diffs-tab a').attr('href') beforeSend: => this.$('.status').addClass 'loading' complete: => diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 4510718c2fd17..5be001e0a226c 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -32,6 +32,9 @@ class Notes # Preview button $(document).on "click", ".js-note-preview-button", @previewNote + # Preview button + $(document).on "click", ".js-note-write-button", @writeNote + # reset main target form after submit $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm @@ -68,6 +71,7 @@ class Notes $(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-attachment-delete" $(document).off "click", ".js-note-preview-button" + $(document).off "click", ".js-note-write-button" $(document).off "ajax:complete", ".js-main-target-form" $(document).off "click", ".js-choose-note-attachment-button" $(document).off "click", ".js-discussion-reply-button" @@ -144,16 +148,36 @@ class Notes # cleanup after successfully creating a diff/discussion note @removeDiscussionNoteForm(form) + ### + Shows write note textarea. + ### + writeNote: (e) -> + e.preventDefault() + form = $(this).closest("form") + # toggle tabs + form.find(".js-note-write-button").parent().addClass "active" + form.find(".js-note-preview-button").parent().removeClass "active" + + # toggle content + form.find(".note-write-holder").show() + form.find(".note-preview-holder").hide() + ### Shows the note preview. Lets the server render GFM into Html and displays it. - - Note: uses the Toggler behavior to toggle preview/edit views/buttons ### previewNote: (e) -> e.preventDefault() form = $(this).closest("form") + # toggle tabs + form.find(".js-note-write-button").parent().removeClass "active" + form.find(".js-note-preview-button").parent().addClass "active" + + # toggle content + form.find(".note-write-holder").hide() + form.find(".note-preview-holder").show() + preview = form.find(".js-note-preview") noteText = form.find(".js-note-text").val() if noteText.trim().length is 0 @@ -179,7 +203,7 @@ class Notes form.find(".js-errors").remove() # reset text and preview - previewContainer = form.find(".js-toggler-container.note_text_and_preview") + previewContainer = form.find(".note-edit-and-preview") previewContainer.removeClass "on" if previewContainer.is(".on") form.find(".js-note-text").val("").trigger "input" diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index a1fe18b02fa38..fbfa72c5e5e92 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -43,12 +43,6 @@ display: none; } } - - .hint { - float: left; - padding: 0; - margin: 0; - } } .div-dropzone-alert { diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index 75cb8e5615236..2758f57bd544b 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -135,10 +135,6 @@ ul.notes { border-width: 1px 0; padding-top: 0; vertical-align: top; - - li { - padding: 5px; - } } } @@ -266,24 +262,33 @@ ul.notes { .clearfix { margin-bottom: 0; } - .note_text_and_preview { - .note_preview { - background: #f5f5f5; - border: 1px solid #ddd; - @include border-radius(4px); - min-height: 80px; - padding: 4px 6px; - - > p { - overflow-x: auto; - } + + .note-preview-holder, + .note_text { + background: #FFF; + border: 1px solid #ddd; + min-height: 100px; + padding: 5px; + font-size: 14px; + box-shadow: none; + } + + .note-preview-holder { + > p { + overflow-x: auto; } - .note_text { + } + + .note_text { + width: 100%; + } + .nav-tabs { + margin-bottom: 0; + border: none; + + li a, + li.active a { border: 1px solid #DDD; - box-shadow: none; - font-size: 14px; - height: 80px; - width: 100%; } } } diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 193c600f7745e..0166b988e580e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -7,7 +7,7 @@ = render "projects/merge_requests/show/participants" - if @commits.present? - %ul.nav.nav-tabs + %ul.nav.nav-tabs.merge-request-tabs %li.notes-tab{data: {action: 'notes'}} = link_to project_merge_request_path(@project, @merge_request) do %i.icon-comment diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index cadae9da8050e..2e84dcea899bb 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -5,21 +5,28 @@ = f.hidden_field :noteable_id = f.hidden_field :noteable_type - .note_text_and_preview.js-toggler-container.notes-container - = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on markdown-area' - .note_preview.js-note-preview.turn-off + %ul.nav.nav-tabs + %li.active + = link_to '#note-write-holder', class: 'js-note-write-button' do + Write + %li + = link_to '#note-preview-holder', class: 'js-note-preview-button', data: { url: preview_project_notes_path(@project) } do + Preview + %div + .note-write-holder + = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input markdown-area' - .hint - .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. - .clearfix - .error-alert + .light.clearfix + .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + + .note-preview-holder.hide + .js-note-preview .note-form-actions .buttons = 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 .note-form-option @@ -30,14 +37,5 @@ %span.file_name.js-attachment-filename File name... = f.file_field :attachment, class: "js-note-attachment-input hidden" - .write-preview-btn - %a.btn.js-note-preview-button.js-toggler-target.turn-off{ href: "javascript:;", data: {url: preview_project_notes_path(@project)} } - %i.icon-eye-open - Preview - %a.btn.btn-primary.js-note-edit-button.js-toggler-target.turn-off{ href: "javascript:;" } - %i.icon-edit - Write - .clearfix - :javascript window.project_image_path_upload = "#{upload_image_project_path @project}"; From 50f3280e019290a1c1434967f0beb24c0e938e9d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 May 2014 13:15:23 +0300 Subject: [PATCH 26/68] Remove unused css Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/behaviors.scss | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss index 64e3c8d9ace2b..23f206ce3dc6c 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -4,20 +4,3 @@ .js-details-container .content.hide { display: block; } .js-details-container.open .content { display: block; } .js-details-container.open .content.hide { display: none; } - -// Toggler -//-------- -.write-preview-btn .turn-on { display: inherit; } -.write-preview-btn .turn-off { display: none; } - -.js-toggler-container .turn-off { display: none; } -.js-toggler-container.on .turn-on { display: none; } -.js-toggler-container.on .turn-off { display: inherit; } - -.js-toggler-container.on ~ .note-form-actions { - .write-preview-btn .turn-on { display: none; } -} - -.js-toggler-container.on ~ .note-form-actions { - .write-preview-btn .turn-off { display: inherit; } -} From 1bb9aeb565fca50304881e36488b202455bdddd2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 May 2014 14:10:36 +0300 Subject: [PATCH 27/68] Fix specs. Fix note form reset after submit Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/notes.js.coffee | 5 ++--- spec/features/notes_on_merge_requests_spec.rb | 2 +- .../features/security/project/internal_access_spec.rb | 11 ----------- spec/features/security/project/private_access_spec.rb | 11 ----------- spec/features/security/project/public_access_spec.rb | 11 ----------- 5 files changed, 3 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 5be001e0a226c..a41ee67a8412b 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -203,8 +203,7 @@ class Notes form.find(".js-errors").remove() # reset text and preview - previewContainer = form.find(".note-edit-and-preview") - previewContainer.removeClass "on" if previewContainer.is(".on") + form.find(".js-note-write-button").click() form.find(".js-note-text").val("").trigger "input" ### @@ -254,7 +253,7 @@ class Notes form.removeClass "js-new-note-form" # setup preview buttons - form.find(".js-note-edit-button, .js-note-preview-button").tooltip placement: "left" + form.find(".js-note-write-button, .js-note-preview-button").tooltip placement: "left" previewButton = form.find(".js-note-preview-button") form.find(".js-note-text").on "input", -> if $(this).val().trim() isnt "" diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 45aebf128c299..7f1da3cffdf79 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -44,7 +44,7 @@ it 'should have text and visible edit button' do within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } - within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } + within(".js-main-target-form") { should have_css(".js-note-write-button", visible: true) } end end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index f6ab47ed91ba9..eb8422df599d3 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -190,17 +190,6 @@ it { should be_denied_for :visitor } end - describe "GET /:project_path/branches/recent" do - subject { recent_project_branches_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_denied_for :visitor } - end - describe "GET /:project_path/branches" do subject { project_branches_path(project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 8a0fcb8e9fffa..186ad35046976 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -168,17 +168,6 @@ it { should be_denied_for :visitor } end - describe "GET /:project_path/branches/recent" do - subject { recent_project_branches_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_denied_for guest } - it { should be_denied_for :user } - it { should be_denied_for :visitor } - end - describe "GET /:project_path/branches" do subject { project_branches_path(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index eb511bfefe02b..f5d1cf7b971cf 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -195,17 +195,6 @@ it { should be_denied_for :visitor } end - describe "GET /:project_path/branches/recent" do - subject { recent_project_branches_path(project) } - - it { should be_allowed_for master } - it { should be_allowed_for reporter } - it { should be_allowed_for :admin } - it { should be_allowed_for guest } - it { should be_allowed_for :user } - it { should be_allowed_for :visitor } - end - describe "GET /:project_path/branches" do subject { project_branches_path(project) } From 5a1429ae2110faaf354574f8e6c7866c29102107 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 May 2014 14:20:12 +0300 Subject: [PATCH 28/68] Fix branches tests Signed-off-by: Dmitriy Zaporozhets --- features/project/commits/branches.feature | 11 ++++------- features/steps/project/browse_branches.rb | 5 ----- features/steps/shared/paths.rb | 4 ++++ 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature index fcf8b7694f453..abebef04fcd97 100644 --- a/features/project/commits/branches.feature +++ b/features/project/commits/branches.feature @@ -3,20 +3,17 @@ Feature: Project Browse branches Given I sign in as a user And I own project "Shop" And project "Shop" has protected branches - Given I visit project branches page - - Scenario: I can see project recent git branches - Then I should see "Shop" recent branches list Scenario: I can see project all git branches - Given I click link "All" + Given I visit project branches page Then I should see "Shop" all branches list Scenario: I can see project protected git branches - Given I click link "Protected" + Given I visit project protected branches page Then I should see "Shop" protected branches list Scenario: I create a branch - Given I click new branch link + Given I visit project branches page + And I click new branch link When I submit new branch form Then I should see new branch created diff --git a/features/steps/project/browse_branches.rb b/features/steps/project/browse_branches.rb index 5b5e8ba493e08..7a0625952de78 100644 --- a/features/steps/project/browse_branches.rb +++ b/features/steps/project/browse_branches.rb @@ -3,11 +3,6 @@ class ProjectBrowseBranches < Spinach::FeatureSteps include SharedProject include SharedPaths - step 'I should see "Shop" recent branches list' do - page.should have_content "Branches" - page.should have_content "master" - end - step 'I click link "All"' do click_link "All" end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index a0213815a781c..d36ff7e3884d3 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -240,6 +240,10 @@ module SharedPaths visit project_branches_path(@project) end + step 'I visit project protected branches page' do + visit project_protected_branches_path(@project) + end + step 'I visit compare refs page' do visit project_compare_index_path(@project) end From bbc4b2be8db5166ce54a81e6f25ffe48fa293a5f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 May 2014 14:26:15 +0300 Subject: [PATCH 29/68] Fix help tests Signed-off-by: Dmitriy Zaporozhets --- app/views/help/raketasks.html.haml | 15 ++------------- features/steps/help.rb | 2 +- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/app/views/help/raketasks.html.haml b/app/views/help/raketasks.html.haml index 17a02e913eec7..61a3774a1322e 100644 --- a/app/views/help/raketasks.html.haml +++ b/app/views/help/raketasks.html.haml @@ -6,8 +6,6 @@ %ul.nav.nav-tabs.log-tabs %li.active - = link_to "Features", "#features", 'data-toggle' => 'tab' - %li = link_to "Maintenance", "#maintenance", 'data-toggle' => 'tab' %li = link_to "User Management", "#user_management", 'data-toggle' => 'tab' @@ -17,16 +15,7 @@ = link_to "Cleanup", "#cleanup", 'data-toggle' => 'tab' .tab-content - .tab-pane.active#features - .file-holder - .file-title - %i.icon-file - Features - .file-content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "raketasks", "features.md")) - - .tab-pane#maintenance + .tab-pane.active#maintenance .file-holder .file-title %i.icon-file @@ -57,7 +46,7 @@ .file-holder .file-title %i.icon-file - Backup & Restore + Backup & Restore .file-content.wiki = preserve do = markdown File.read(Rails.root.join("doc", "raketasks", "backup_restore.md")) diff --git a/features/steps/help.rb b/features/steps/help.rb index aa147fd65cef2..5ea2c5daeebb7 100644 --- a/features/steps/help.rb +++ b/features/steps/help.rb @@ -16,6 +16,6 @@ class Spinach::Features::Help < Spinach::FeatureSteps end step 'Header "Rebuild project satellites" should have correct ids and links' do - header_should_have_correct_id_and_link(3, 'Rebuild project satellites', 'rebuild-project-satellites') + header_should_have_correct_id_and_link(3, '(Re-)Create satellite repos', 're-create-satellite-repos') end end From ef933ae69bb48fd186c650927bff7d52a3956174 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 May 2014 14:41:13 +0300 Subject: [PATCH 30/68] Even more tests fixed :) Signed-off-by: Dmitriy Zaporozhets --- features/steps/shared/diff_note.rb | 2 +- features/steps/shared/note.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index f80d8d064755d..f917d7bde08c1 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -138,7 +138,7 @@ module SharedDiffNote Then 'I should see the diff comment edit button' do within(".diff-file") do - page.should have_css(".js-note-edit-button", visible: true) + page.should have_css(".js-note-write-button", visible: true) end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 36b81b74186dc..1c52d4c72d89a 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -81,7 +81,7 @@ module SharedNote Then 'I should see the comment edit button' do within(".js-main-target-form") do - page.should have_css(".js-note-edit-button", visible: true) + page.should have_css(".js-note-write-button", visible: true) end end From cc1dd324643db07bdb29f3673995a8ad0dffa2df Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sat, 24 May 2014 20:05:02 +0300 Subject: [PATCH 31/68] Add pkgr.io to readme one-click installation methods --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cbbfebc81efb6..283336b9e4d8c 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ * [Cloud 66 deployment and management](http://blog.cloud66.com/installing-gitlab-ubuntu/) Use Cloud 66 to deploy GitLab to your own server or any cloud (eg. DigitalOcean, AWS, Rackspace, GCE) and then manage it with database backups, scaling and more. +* [Pkgr.io one-click installer](https://pkgr.io/apps/gitlabhq/gitlabhq) Currently supporting Ubuntu 14.04, Ubuntu 12.04 and Debian 7.4. For more information check the [README](https://gitlab.com/gitlab-org/gitlab-recipes/blob/master/install/pkgr/README.md) at gitlab-recipes repo. + #### Unofficial installation methods * [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/) repository with unofficial guides for using GitLab with different software (operating systems, webservers, etc.) than the official version. From 23bcb9cb0d294611c3bcd1b453dd67ee73d50dff Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 09:09:27 +0200 Subject: [PATCH 32/68] A regressions issue to improve communication. --- doc/release/monthly.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index ed580f666ac05..667e3048a52a7 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -201,6 +201,15 @@ Include a link to the blog post and keep it short. Proposed email for CE: "We have released a new version of GitLab Community Edition and its packages. See our blog post() for more information." +### **10. Create a regressions issue** + +On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create and issue titled "GitLab X.X.X regressions", assign it to the release manager, /cc all the core-team members active on the issue tracker add the following text: + +This is a meta issue to discuss possible regressions in this release and any patch versions. +Please do not raise issues directly in this issue but link to issues that might warrant a patch release. +The decision to create a patch release or not is with the release manager who is assigned to this issue. +The release manager will comment here about the plans for patch releases. + # **23rd - Optional Patch Release** # **24th - Update GitLab.com** From 9f6709d8c4bcd72cfa7c52f7e3158da6d117b273 Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 09:11:09 +0200 Subject: [PATCH 33/68] Have the issue for the entire monthly time. --- doc/release/monthly.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 667e3048a52a7..e538baa375e70 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -203,9 +203,9 @@ Proposed email for CE: "We have released a new version of GitLab Community Editi ### **10. Create a regressions issue** -On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create and issue titled "GitLab X.X.X regressions", assign it to the release manager, /cc all the core-team members active on the issue tracker add the following text: +On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create and issue titled "GitLab X.X regressions", assign it to the release manager, /cc all the core-team members active on the issue tracker add the following text: -This is a meta issue to discuss possible regressions in this release and any patch versions. +This is a meta issue to discuss possible regressions in this monthly release and any patch versions. Please do not raise issues directly in this issue but link to issues that might warrant a patch release. The decision to create a patch release or not is with the release manager who is assigned to this issue. The release manager will comment here about the plans for patch releases. From f39d8a8a6006ab8d5e3aa747036be586684718aa Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 09:12:57 +0200 Subject: [PATCH 34/68] Add any known bugs. --- doc/release/monthly.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index e538baa375e70..19c310ac3e334 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -203,13 +203,15 @@ Proposed email for CE: "We have released a new version of GitLab Community Editi ### **10. Create a regressions issue** -On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create and issue titled "GitLab X.X regressions", assign it to the release manager, /cc all the core-team members active on the issue tracker add the following text: +On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create and issue titled "GitLab X.X regressions" add the following text: This is a meta issue to discuss possible regressions in this monthly release and any patch versions. Please do not raise issues directly in this issue but link to issues that might warrant a patch release. The decision to create a patch release or not is with the release manager who is assigned to this issue. The release manager will comment here about the plans for patch releases. +Assign the issue to the release manager and /cc all the core-team members active on the issue tracker. If there are any known bugs in the release add them immediately. + # **23rd - Optional Patch Release** # **24th - Update GitLab.com** From 1748073315e18a1211ca9502d5d227e95d844bcc Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 09:14:44 +0200 Subject: [PATCH 35/68] Correct typo. --- doc/release/monthly.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 19c310ac3e334..390ec4226079a 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -203,7 +203,7 @@ Proposed email for CE: "We have released a new version of GitLab Community Editi ### **10. Create a regressions issue** -On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create and issue titled "GitLab X.X regressions" add the following text: +On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create an issue titled "GitLab X.X regressions" add the following text: This is a meta issue to discuss possible regressions in this monthly release and any patch versions. Please do not raise issues directly in this issue but link to issues that might warrant a patch release. From 8bec6b0bcb100b30a43fcd9c6649d1bee113b6a7 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 May 2014 14:17:46 +0200 Subject: [PATCH 36/68] Make existing tests test something, return correct errors. --- app/controllers/projects_controller.rb | 19 ++++++++++++++----- app/uploaders/file_uploader.rb | 2 +- spec/controllers/projects_controller_spec.rb | 17 +++++++++-------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c15205fb68fb6..3144ece977c97 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -164,12 +164,21 @@ def unarchive def upload_image uploader = FileUploader.new('uploads', upload_path, accepted_images) - alt = params['markdown_img'].original_filename - uploader.store!(params['markdown_img']) - link = { 'alt' => File.basename(alt, '.*'), - 'url' => File.join(root_url, uploader.url) } + image = params['markdown_img'] + + if image && accepted_images.map{ |format| image.content_type.include? format }.any? + alt = image.original_filename + uploader.store!(image) + link = { 'alt' => File.basename(alt, '.*'), + 'url' => File.join(root_url, uploader.url) } + end + respond_to do |format| - format.json { render json: { link: link } } + if link + format.json { render json: { link: link } } + else + format.json { render json: "Invalid file.", status: :unprocessable_entity } + end end end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index cbc9271ac14c8..0fa987c93f641 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -25,7 +25,7 @@ def extension_white_list end def store!(file) - file.original_filename = self.class.generate_filename(file) + @filename = self.class.generate_filename(file) super end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 07ca8d2502665..1d465d4996e69 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -11,34 +11,35 @@ describe "POST #upload_image" do before do sign_in(user) + project.team << [user, :developer] end context "without params['markdown_img']" do it "returns an error" do - post :upload_image, id: project.to_param - expect(response.status).to eq(404) + post :upload_image, id: project.to_param, format: :json + expect(response.status).to eq(422) end end context "with invalid file" do before do - post :upload_image, id: project.to_param, markdown_img: @img + post :upload_image, id: project.to_param, markdown_img: txt, format: :json end it "returns an error" do - expect(response.status).to eq(404) + expect(response.status).to eq(422) end end context "with valid file" do before do - post :upload_image, id: project.to_param, markdown_img: @img + post :upload_image, id: project.to_param, markdown_img: jpg, format: :json end it "returns a content with original filename and new link." do - link = { alt: 'rails_sample', link: '' }.to_json - expect(response.body).to have_content link + expect(response.body).to match "\"alt\":\"rails_sample\"" + expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}" end end end -end \ No newline at end of file +end From 3b2b3cff04993c7247e953c10aa8c6fb5e8d6ddb Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 May 2014 14:53:34 +0200 Subject: [PATCH 37/68] Move logic to image_service. --- app/controllers/projects_controller.rb | 14 ++------ app/services/projects/image_service.rb | 39 +++++++++++++++++++++ spec/controllers/commits_controller_spec.rb | 2 +- 3 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 app/services/projects/image_service.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3144ece977c97..07ccbd57fafb6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -163,19 +163,11 @@ def unarchive end def upload_image - uploader = FileUploader.new('uploads', upload_path, accepted_images) - image = params['markdown_img'] - - if image && accepted_images.map{ |format| image.content_type.include? format }.any? - alt = image.original_filename - uploader.store!(image) - link = { 'alt' => File.basename(alt, '.*'), - 'url' => File.join(root_url, uploader.url) } - end + link_to_image = ::Projects::ImageService.new(repository, params, root_url).execute respond_to do |format| - if link - format.json { render json: { link: link } } + if link_to_image + format.json { render json: { link: link_to_image } } else format.json { render json: "Invalid file.", status: :unprocessable_entity } end diff --git a/app/services/projects/image_service.rb b/app/services/projects/image_service.rb new file mode 100644 index 0000000000000..c79ddddd97200 --- /dev/null +++ b/app/services/projects/image_service.rb @@ -0,0 +1,39 @@ +module Projects + class ImageService < BaseService + include Rails.application.routes.url_helpers + def initialize(repository, params, root_url) + @repository, @params, @root_url = repository, params.dup, root_url + end + + def execute + uploader = FileUploader.new('uploads', upload_path, accepted_images) + image = @params['markdown_img'] + + if image && correct_mime_type?(image) + alt = image.original_filename + uploader.store!(image) + link = { + 'alt' => File.basename(alt, '.*'), + 'url' => File.join(@root_url, uploader.url) + } + else + link = nil + end + end + + protected + + def upload_path + base_dir = FileUploader.generate_dir + File.join(@repository.path_with_namespace, base_dir) + end + + def accepted_images + %w(png jpg jpeg gif) + end + + def correct_mime_type?(image) + accepted_images.map{ |format| image.content_type.include? format }.any? + end + end +end diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/commits_controller_spec.rb index 308cfa692192e..0c19d755eb188 100644 --- a/spec/controllers/commits_controller_spec.rb +++ b/spec/controllers/commits_controller_spec.rb @@ -6,7 +6,7 @@ before do sign_in(user) - project.creator = user + project.team << [user, :master] end describe "GET show" do From f441436e53377f207657ac0e0e518e5ee2b33a6c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 May 2014 16:08:22 +0300 Subject: [PATCH 38/68] Add compare branches endpoint to API Signed-off-by: Dmitriy Zaporozhets --- doc/api/repositories.md | 66 +++++++++++++++++++++++++++++++++++++++++ lib/api/entities.rb | 17 +++++++++++ lib/api/repositories.rb | 16 ++++++++++ 3 files changed, 99 insertions(+) diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 858fad0a0b508..2cd387275e944 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -131,3 +131,69 @@ GET /projects/:id/repository/archive Parameters: + `id` (required) - The ID of a project + `sha` (optional) - The commit sha to download defaults to the tip of the default branch + + +## Compare branches, tags or commits + +``` +GET /projects/:id/repository/compare +``` + +Parameters: ++ `id` (required) - The ID of a project ++ `from` (required) - the commit sha or branch name ++ `to` (required) - the commit sha or branch name + + +``` +GET /projects/:id/repository/compare?from=master&to=feature +``` + +Response: + +```json +{ + "commit": { + "id": "72e10ef47e770a95439255b2c49de722e8782106", + "short_id": "72e10ef47e7", + "title": "Add NEWFILE", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "created_at": "2014-05-26T16:03:54+03:00" + }, + "commits": [{ + "id": "0b4bc9a49b562e85de7cc9e834518ea6828729b9", + "short_id": "0b4bc9a49b5", + "title": "Feature added", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "created_at": "2014-02-27T10:26:01+02:00" + }, { + "id": "72e10ef47e770a95439255b2c49de722e8782106", + "short_id": "72e10ef47e7", + "title": "Add NEWFILE", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "created_at": "2014-05-26T16:03:54+03:00" + }], + "diffs": [{ + "old_path": "NEWFILE", + "new_path": "NEWFILE", + "a_mode": null, + "b_mode": null, + "diff": "--- /dev/null\n+++ b/NEWFILE\n@@ -0,0 +1 @@\n+This is NEWFILE content\n\\ No newline at end of file", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }, { + "old_path": "files/ruby/feature.rb", + "new_path": "files/ruby/feature.rb", + "a_mode": null, + "b_mode": null, + "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }] +} +``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 457af52fe9dad..eead8b18ebd46 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -194,5 +194,22 @@ class ProjectWithAccess < Project class Label < Grape::Entity expose :name end + + class RepoDiff < Grape::Entity + expose :old_path, :new_path, :a_mode, :b_mode, :diff + expose :new_file, :renamed_file, :deleted_file + end + + class Compare < Grape::Entity + expose :commit, using: Entities::RepoCommit do |compare, options| + Commit.new compare.commit + end + expose :commits, using: Entities::RepoCommit do |compare, options| + Commit.decorate compare.commits + end + expose :diffs, using: Entities::RepoDiff do |compare, options| + compare.diffs + end + end end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 076a9ceeb744e..d59c25cf3167d 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -15,6 +15,7 @@ def handle_project_member_errors(errors) not_found! end end + # Get a project repository tags # # Parameters: @@ -118,6 +119,21 @@ def handle_project_member_errors(errors) not_found! end end + + # Compare two branches, tags or commits + # + # Parameters: + # id (required) - The ID of a project + # from (required) - the commit sha or branch name + # to (required) - the commit sha or branch name + # Example Request: + # GET /projects/:id/repository/compare?from=master&to=feature + get ':id/repository/compare' do + authorize! :download_code, user_project + compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to], MergeRequestDiff::COMMITS_SAFE_SIZE) + + present compare, with: Entities::Compare + end end end end From 75e90cb45660bd9861a047d0cd4f630ca2e71dd3 Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 15:38:18 +0200 Subject: [PATCH 39/68] Push to all remotes doc. --- doc/release/README.md | 8 ++++++-- doc/release/master.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 doc/release/master.md diff --git a/doc/release/README.md b/doc/release/README.md index 22510be3f1802..d53720acbd427 100644 --- a/doc/release/README.md +++ b/doc/release/README.md @@ -1,2 +1,6 @@ -+ [Monthly](monthly.md) -+ [Security](security.md) +GitLab has the following updates: + ++ [Monthly release](monthly.md), every month on the 22nd. ++ [Patch release](patch.md), if there are serious regressions. ++ [Security](security.md), for security problems. ++ [Master](master.md), update process for the master branch. diff --git a/doc/release/master.md b/doc/release/master.md new file mode 100644 index 0000000000000..aa7c854d0742e --- /dev/null +++ b/doc/release/master.md @@ -0,0 +1,29 @@ +# How to push GitLab CE master branch to all remotes. + +Distribution to other repo's is done by Dmitriy if there is no rush. If a GitLab B.V. person wants to do it here are the instructions. + +## Add this to `.bashrc` + +```bash +gpa () +{ + git push origin $1 && git push gh $1 && git push public $1 +} +``` + +## Then add remotes to your local repo + +```bash +cd myrepo + +git remote add origin git@dev.gitlab.org:gitlab/gitlabhq.git +git remote add gh git@github.com:gitlabhq/gitlabhq.git +git remote add public git@gitlab.com:gitlab-org/gitlab-ce.git +``` + +## Pushto all remotes + +```bash +gpa master +``` + From c7a55309308aeffb65ac79dbe4696fa87a0e9f7c Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 15:40:44 +0200 Subject: [PATCH 40/68] Small layout improvements. --- doc/release/master.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/release/master.md b/doc/release/master.md index aa7c854d0742e..a3ad6e3886f7b 100644 --- a/doc/release/master.md +++ b/doc/release/master.md @@ -1,6 +1,8 @@ # How to push GitLab CE master branch to all remotes. -Distribution to other repo's is done by Dmitriy if there is no rush. If a GitLab B.V. person wants to do it here are the instructions. +Distribution to other repo's is done by the lead developer if there is no rush. +This happens a few times per workday on average. +If a GitLab B.V. person wants to do it here are the instructions. ## Add this to `.bashrc` @@ -11,7 +13,7 @@ gpa () } ``` -## Then add remotes to your local repo +## Then add remotes to your local repo ```bash cd myrepo @@ -21,7 +23,7 @@ git remote add gh git@github.com:gitlabhq/gitlabhq.git git remote add public git@gitlab.com:gitlab-org/gitlab-ce.git ``` -## Pushto all remotes +## Push to all remotes ```bash gpa master From f8a6d3405ea3a25f5b0776241831deb037020fc6 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 May 2014 15:47:54 +0200 Subject: [PATCH 41/68] Add image_service spec. --- spec/controllers/projects_controller_spec.rb | 3 - spec/services/projects/image_service_spec.rb | 65 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 spec/services/projects/image_service_spec.rb diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 1d465d4996e69..7a772396e8b63 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -3,10 +3,7 @@ describe ProjectsController do let(:project) { create(:project) } let(:user) { create(:user) } - let(:png) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') } let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } - let(:gif) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } describe "POST #upload_image" do before do diff --git a/spec/services/projects/image_service_spec.rb b/spec/services/projects/image_service_spec.rb new file mode 100644 index 0000000000000..0311e312b5ddf --- /dev/null +++ b/spec/services/projects/image_service_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Projects::ImageService do + before(:each) { enable_observers } + after(:each) { disable_observers } + + describe 'Image service' do + before do + @user = create :user + @project = create :project, creator_id: @user.id, namespace: @user.namespace + end + + context 'for valid gif file' do + before do + gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + @link_to_image = upload_image(@project.repository, { 'markdown_img' => gif }, "http://test.example/") + end + + it { expect(@link_to_image).to have_key("alt") } + it { expect(@link_to_image).to have_key("url") } + it { expect(@link_to_image).to have_value("banana_sample") } + it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_image["url"]).to match("banana_sample.gif") } + end + + context 'for valid png file' do + before do + png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') + @link_to_image = upload_image(@project.repository, { 'markdown_img' => png }, "http://test.example/") + end + + it { expect(@link_to_image).to have_key("alt") } + it { expect(@link_to_image).to have_key("url") } + it { expect(@link_to_image).to have_value("dk") } + it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_image["url"]).to match("dk.png") } + end + + context 'for valid jpg file' do + before do + jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') + @link_to_image = upload_image(@project.repository, { 'markdown_img' => jpg }, "http://test.example/") + end + + it { expect(@link_to_image).to have_key("alt") } + it { expect(@link_to_image).to have_key("url") } + it { expect(@link_to_image).to have_value("rails_sample") } + it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_image["url"]).to match("rails_sample.jpg") } + end + + context 'for txt file' do + before do + txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') + @link_to_image = upload_image(@project.repository, { 'markdown_img' => txt }, "http://test.example/") + end + + it { expect(@link_to_image).to be_nil } + end + end + + def upload_image(repository, params, root_url) + Projects::ImageService.new(repository, params, root_url).execute + end +end From fe10f37199a04b92dd5bf2b713d49849491f9dfc Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 15:54:06 +0200 Subject: [PATCH 42/68] Add default arguments, name gitlab repo gl, suggest dotfile repo. --- doc/release/master.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/release/master.md b/doc/release/master.md index a3ad6e3886f7b..bed5692d0a2c0 100644 --- a/doc/release/master.md +++ b/doc/release/master.md @@ -4,28 +4,28 @@ Distribution to other repo's is done by the lead developer if there is no rush. This happens a few times per workday on average. If a GitLab B.V. person wants to do it here are the instructions. -## Add this to `.bashrc` +## Add this to `.bashrc` or [your dotfiles](https://github.com/dosire/dotfiles/commit/52803ce3ac60d57632164b7713ff0041e86fa26c) ```bash gpa () { - git push origin $1 && git push gh $1 && git push public $1 + git push origin ${1:-master} && git push gh ${1:-master} && git push gl ${1:-master} } ``` ## Then add remotes to your local repo ```bash -cd myrepo +cd my-gitlab-ce-repo git remote add origin git@dev.gitlab.org:gitlab/gitlabhq.git -git remote add gh git@github.com:gitlabhq/gitlabhq.git -git remote add public git@gitlab.com:gitlab-org/gitlab-ce.git +git remote add gh git@github.com:gitlabhq/gitlabhq.git +git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git ``` ## Push to all remotes ```bash -gpa master +gpa ``` From 43e7b8fa075a2744b2debf11f52d1df731950aa8 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 May 2014 15:55:41 +0200 Subject: [PATCH 43/68] Remove empty space --- spec/services/projects/image_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/projects/image_service_spec.rb b/spec/services/projects/image_service_spec.rb index 0311e312b5ddf..070c21698cf5f 100644 --- a/spec/services/projects/image_service_spec.rb +++ b/spec/services/projects/image_service_spec.rb @@ -3,7 +3,7 @@ describe Projects::ImageService do before(:each) { enable_observers } after(:each) { disable_observers } - + describe 'Image service' do before do @user = create :user From 9b2a13497dfc1fc97ac5d84f4074729cda040e2a Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 May 2014 15:58:03 +0200 Subject: [PATCH 44/68] Need txt file in test. --- spec/controllers/projects_controller_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 7a772396e8b63..944df5314bd04 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -4,6 +4,7 @@ let(:project) { create(:project) } let(:user) { create(:user) } let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } describe "POST #upload_image" do before do From 040b3be5b1e1a0212c4c0073dfb252c26adbfc7d Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 16:15:12 +0200 Subject: [PATCH 45/68] Give a hint where to find the email settings. --- config/gitlab.yml.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 7b53a0655336e..04e85ed9a9de2 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -38,6 +38,8 @@ production: &base # Email address of your support contact (default: same as email_from) support_email: support@example.com + # Email server smtp settings are in [a separate file](initializers/smtp_settings.rb.sample). + ## User settings default_projects_limit: 10 # default_can_create_group: false # default: true From 242291850d7513731355c42e67c2f8c3cc727451 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 May 2014 18:05:55 +0300 Subject: [PATCH 46/68] Add some tests to compare api Signed-off-by: Dmitriy Zaporozhets --- spec/requests/api/repositories_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 5a5222ed3c52b..690a70b0b1612 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -112,4 +112,24 @@ response.status.should == 404 end end + + describe 'GET /GET /projects/:id/repository/compare' do + it "should compare 2 branches" do + get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'simple_merge_request' + response.status.should == 200 + json_response['commits'].size.should == 3 + end + + it "should compare 2 commits" do + get api("/projects/#{project.id}/repository/compare", user), from: 'b1e6a9dbf1c85', to: '1e689bfba395' + response.status.should == 200 + json_response['commits'].size.should == 0 + end + + it "should compare 2 commits" do + get api("/projects/#{project.id}/repository/compare", user), from: '1e689bfba395', to: 'b1e6a9dbf1c85' + response.status.should == 200 + json_response['commits'].size.should == 4 + end + end end From b2ec09e5d6e430eb4d4fd248364de9fe3bc9a116 Mon Sep 17 00:00:00 2001 From: dosire Date: Mon, 26 May 2014 17:09:27 +0200 Subject: [PATCH 47/68] Specify what version to test manually. --- doc/release/monthly.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index ed580f666ac05..d639c44a5eb10 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -120,7 +120,7 @@ It is important to do this as soon as possible, so we can catch any errors befor Create issue on dev.gitlab.org gitlab repository, named "GitLab X.X release" in order to keep track of the progress. -Use the omnibus packages or cookbook to test using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md). +Use the omnibus packages of Enterprise Edition using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md). **NOTE** Upgrader can only be tested when tags are pushed to all repositories. Do not forget to confirm it is working before releasing. Note that in the issue. From 52c3179be94e857ecb0de2cfa8ecbd484a36d213 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 20 May 2014 15:29:14 +0200 Subject: [PATCH 48/68] Do not replace links inside code blocks, less code for the same amount of work. --- app/helpers/gitlab_markdown_helper.rb | 109 ++++++++++---------------- lib/redcarpet/render/gitlab_html.rb | 17 +--- 2 files changed, 43 insertions(+), 83 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 69425bc171d81..2d226adb2a495 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -59,90 +59,59 @@ def render_wiki_content(wiki_page) end end - # text - whole text from a markdown file - # project_path_with_namespace - namespace/projectname, eg. gitlabhq/gitlabhq - # ref - name of the branch or reference, eg. stable - # requested_path - path of request, eg. doc/api/README.md, used in special case when path is pointing to the .md file were the original request is coming from - def create_relative_links(text, project, ref, requested_path) - @path_to_satellite = project.satellite.path - project_path_with_namespace = project.path_with_namespace + def create_relative_links(text) paths = extract_paths(text) - paths.each do |file_path| - original_file_path = extract(file_path) - new_path = rebuild_path(project_path_with_namespace, original_file_path, requested_path, ref) - if reference_path?(file_path) - # Replacing old string with a new one that contains updated path - # eg. [some document]: document.md will be replaced with [some document] /namespace/project/master/blob/document.md - text.gsub!(file_path, file_path.gsub(original_file_path, "/#{new_path}")) - else - # Replacing old string with a new one with brackets ]() to prevent replacing occurence of a word - # e.g. If we have a markdown like [test](test) this will replace ](test) and not the word test - text.gsub!("](#{file_path})", "](/#{new_path})") - end - end - text - end - def extract_paths(markdown_text) - all_markdown_paths = pick_out_paths(markdown_text) - paths = remove_empty(all_markdown_paths) - select_relative(paths) - end + paths.uniq.each do |file_path| + 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 - # Split the markdown text to each line and find all paths, this will match anything with - ]("some_text") and [some text]: file.md - def pick_out_paths(markdown_text) - inline_paths = markdown_text.split("\n").map { |text| text.scan(/\]\(([^(]+)\)/) } - reference_paths = markdown_text.split("\n").map { |text| text.scan(/\[.*\]:.*/) } - inline_paths + reference_paths + text end - # Removes any empty result produced by not matching the regexp - def remove_empty(paths) - paths.reject{|l| l.empty? }.flatten + def extract_paths(text) + links = substitute_links(text) + image_links = substitute_image_links(text) + links + image_links end - # If a path is a reference style link we need to omit ]: - def extract(path) - path.split("]: ").last + def substitute_links(text) + links = text.scan(//) + relative_links = links.flatten.reject{ |link| link_to_ignore? link } + relative_links end - # Reject any path that contains ignored protocol - # eg. reject "https://gitlab.org} but accept "doc/api/README.md" - def select_relative(paths) - paths.reject{|path| ignored_protocols.map{|protocol| path.include?(protocol)}.any?} + def substitute_image_links(text) + links = text.scan(/ Date: Tue, 20 May 2014 15:58:30 +0200 Subject: [PATCH 49/68] Make sure slashes are handled properly. --- app/helpers/gitlab_markdown_helper.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 2d226adb2a495..b8891d801aaed 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -102,6 +102,8 @@ def rebuild_path(path) path.gsub!(/(#.*)/, "") id = $1 || "" file_path = relative_file_path(path) + file_path = sanitize_slashes(file_path) + [ Gitlab.config.gitlab.relative_url_root, @project.path_with_namespace, @@ -110,6 +112,12 @@ def rebuild_path(path) ].compact.join("/").gsub(/^\/*|\/*$/, '') + 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) From 281643a1bfd6f4da88a278bfce20133c80105de5 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 23 May 2014 12:58:34 +0200 Subject: [PATCH 50/68] Add feature spec. --- features/project/issues/issues.feature | 6 ++++++ features/steps/project/issues.rb | 12 ++++++++++++ lib/redcarpet/render/gitlab_html.rb | 6 +++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index c5311544efa5a..191e8dcbe7fd7 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -68,6 +68,12 @@ Feature: Project Issues And I leave a comment with a header containing "Comment with a header" Then The comment with the header should not have an ID + @javascript + Scenario: Blocks inside comments should not build relative links + Given I visit issue page "Release 0.4" + And I leave a comment with code block + Then The code block should be unchanged + Scenario: Issues on empty project Given empty project "Empty Project" When I visit empty project page diff --git a/features/steps/project/issues.rb b/features/steps/project/issues.rb index d1f3ba25a21b8..d0b4aa6e08009 100644 --- a/features/steps/project/issues.rb +++ b/features/steps/project/issues.rb @@ -163,4 +163,16 @@ class ProjectIssues < Spinach::FeatureSteps project = Project.find_by(name: 'Empty Project') visit project_issues_path(project) end + + step 'I leave a comment with code block' do + 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 + 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```") + end end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 29bf42e762644..bb225f1acd8a2 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -6,8 +6,6 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML def initialize(template, options = {}) @template = template @project = @template.instance_variable_get("@project") - @ref = @template.instance_variable_get("@ref") - @request_path = @template.instance_variable_get("@path") @options = options.dup super options end @@ -46,7 +44,9 @@ def header(text, level) end def postprocess(full_document) - full_document = h.create_relative_links(full_document) + unless @template.instance_variable_get("@project_wiki") || @project.nil? + full_document = h.create_relative_links(full_document) + end h.gfm(full_document) end end From e426d43924be265d4834f47fd765895dde6017fb Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 May 2014 21:54:57 +0300 Subject: [PATCH 51/68] Check return system value for gitlab test rake tasks Signed-off-by: Dmitriy Zaporozhets --- lib/tasks/gitlab/test.rake | 5 ++--- lib/tasks/spec.rake | 4 ++-- lib/tasks/spinach.rake | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index 9516210e205ff..f38fe7810c14f 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -8,9 +8,8 @@ namespace :gitlab do ] cmds.each do |cmd| - result = system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) - - raise "#{cmd} failed!" unless result + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) || + raise "#{cmd} failed!" end end end diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index 49fbe1bd47ae7..a7cd7483bed1b 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -40,7 +40,7 @@ end def run_commands(cmds) cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) - raise "#{cmd} failed!" unless $?.exitstatus.zero? + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) || + raise "#{cmd} failed!" end end diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index c23d0e0e188fa..ebcce1e650ce8 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -8,7 +8,7 @@ task :spinach do ] cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) - raise "#{cmd} failed!" unless $?.exitstatus.zero? + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) || + raise "#{cmd} failed!" end end From c456231690c8fabeeefc844d46c9d5b0403df357 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 May 2014 23:26:41 +0300 Subject: [PATCH 52/68] Use css truncation on dashboard --- app/assets/stylesheets/generic/common.scss | 7 +------ app/assets/stylesheets/main/mixins.scss | 9 +++++++++ app/assets/stylesheets/sections/events.scss | 17 +++++++++++++++++ app/views/events/_event.html.haml | 2 +- app/views/events/_event_last_push.html.haml | 13 +++++++------ app/views/events/event/_push.html.haml | 2 +- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 4e660d0b1e072..6ab6458ea0dc8 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -71,12 +71,7 @@ pre { } .str-truncated { - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: top; - white-space: nowrap; - max-width: 82%; + @include str-truncated; } /** FLASH message **/ diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/main/mixins.scss index 8143cfa2c8107..747676620b319 100644 --- a/app/assets/stylesheets/main/mixins.scss +++ b/app/assets/stylesheets/main/mixins.scss @@ -123,3 +123,12 @@ margin-top: 0px; margin-bottom: 15px; } + +@mixin str-truncated($max_width: "82%") { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + max-width: $max_width; +} diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/sections/events.scss index b68f882220ec5..05361976efc63 100644 --- a/app/assets/stylesheets/sections/events.scss +++ b/app/assets/stylesheets/sections/events.scss @@ -45,6 +45,7 @@ padding: 12px 0px; border-bottom: 1px solid #eee; .event-title { + @include str-truncated(72%); color: #333; font-weight: normal; font-size: 14px; @@ -135,6 +136,12 @@ } } } + + .event-item-timestamp { + float: right; + color: #999; + line-height: 22px; + } } /** @@ -166,3 +173,13 @@ } } } + +/* + * Last push widget + */ +.event-last-push { + .event-last-push-text { + @include str-truncated(75%); + line-height: 24px; + } +} diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 8cf26671e3bf9..613833153739e 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,6 +1,6 @@ - if event.proper? .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"} - %span.cgray.pull-right + .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} = cache event do diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index 6db05a1a5a6ef..4c9a39bcc272b 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -1,11 +1,12 @@ - if show_last_push_widget?(event) .event-last-push - %span You pushed to - = link_to project_commits_path(event.project, event.ref_name) do - %strong= truncate(event.ref_name, length: 28) - at - %strong= link_to_project event.project - #{time_ago_with_tooltip(event.created_at)} + .event-last-push-text + %span You pushed to + = link_to project_commits_path(event.project, event.ref_name) do + %strong= event.ref_name + at + %strong= link_to_project event.project + #{time_ago_with_tooltip(event.created_at)} .pull-right = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-small" do diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index f181df23eb4c6..1bca64c7d508c 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -5,7 +5,7 @@ %strong= event.ref_name - else = link_to project_commits_path(event.project, event.ref_name) do - %strong= truncate(event.ref_name, length: 30) + %strong= event.ref_name at = link_to_project event.project From c9020ae5866527f0642b00bea42b71ed0476d0e4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 May 2014 20:30:45 +0000 Subject: [PATCH 53/68] Fix test.rake --- lib/tasks/gitlab/test.rake | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index f38fe7810c14f..5b937ce0a2895 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -8,8 +8,7 @@ namespace :gitlab do ] cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) || - raise "#{cmd} failed!" + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") end end -end +end \ No newline at end of file From 1e7fa8d99b52d89571b8e9581746561d00c383d5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 May 2014 08:12:03 +0300 Subject: [PATCH 54/68] Fix syntax error Signed-off-by: Dmitriy Zaporozhets --- lib/tasks/spec.rake | 3 +-- lib/tasks/spinach.rake | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index a7cd7483bed1b..bee2230029815 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -40,7 +40,6 @@ end def run_commands(cmds) cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) || - raise "#{cmd} failed!" + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") end end diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index ebcce1e650ce8..dcc7d0fe01c24 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -8,7 +8,6 @@ task :spinach do ] cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) || - raise "#{cmd} failed!" + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") end end From f4889f50fcd9ec8df9a1f44def25bb9ea384a3bf Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 27 May 2014 08:58:26 +0200 Subject: [PATCH 55/68] Add entry to changelog. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d265d3a0b41cd..52ee3cc5d605f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 7.0.0 - Move protected branches page to Project settings area - Redirect to Files view when create new branch via UI - Drag and drop upload of image in every markdown-area (Earle Randolph Bunao and Neil Francis Calabroso) + - Refactor the markdown relative links processing v 6.9.0 - Store Rails cache data in the Redis `cache:gitlab` namespace From 10ee137e9c13271d49d7046723b1e473d62a578f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 May 2014 11:16:50 +0300 Subject: [PATCH 56/68] Expose timeout and same_ref compare fields Signed-off-by: Dmitriy Zaporozhets --- doc/api/repositories.md | 47 ++++++++++---------------- lib/api/entities.rb | 6 ++++ lib/api/repositories.rb | 2 +- spec/requests/api/repositories_spec.rb | 3 ++ 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 2cd387275e944..e9120e17bb82d 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -152,48 +152,35 @@ GET /projects/:id/repository/compare?from=master&to=feature Response: ```json + { "commit": { - "id": "72e10ef47e770a95439255b2c49de722e8782106", - "short_id": "72e10ef47e7", - "title": "Add NEWFILE", + "id": "12d65c8dd2b2676fa3ac47d955accc085a37a9c1", + "short_id": "12d65c8dd2b", + "title": "JS fix", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", - "created_at": "2014-05-26T16:03:54+03:00" + "created_at": "2014-02-27T10:27:00+02:00" }, "commits": [{ - "id": "0b4bc9a49b562e85de7cc9e834518ea6828729b9", - "short_id": "0b4bc9a49b5", - "title": "Feature added", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "created_at": "2014-02-27T10:26:01+02:00" - }, { - "id": "72e10ef47e770a95439255b2c49de722e8782106", - "short_id": "72e10ef47e7", - "title": "Add NEWFILE", + "id": "12d65c8dd2b2676fa3ac47d955accc085a37a9c1", + "short_id": "12d65c8dd2b", + "title": "JS fix", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", - "created_at": "2014-05-26T16:03:54+03:00" + "created_at": "2014-02-27T10:27:00+02:00" }], "diffs": [{ - "old_path": "NEWFILE", - "new_path": "NEWFILE", + "old_path": "files/js/application.js", + "new_path": "files/js/application.js", "a_mode": null, - "b_mode": null, - "diff": "--- /dev/null\n+++ b/NEWFILE\n@@ -0,0 +1 @@\n+This is NEWFILE content\n\\ No newline at end of file", - "new_file": true, + "b_mode": "100644", + "diff": "--- a/files/js/application.js\n+++ b/files/js/application.js\n@@ -24,8 +24,10 @@\n //= require g.raphael-min\n //= require g.bar-min\n //= require branch-graph\n-//= require highlightjs.min\n-//= require ace/ace\n //= require_tree .\n //= require d3\n //= require underscore\n+\n+function fix() { \n+ alert(\"Fixed\")\n+}", + "new_file": false, "renamed_file": false, "deleted_file": false - }, { - "old_path": "files/ruby/feature.rb", - "new_path": "files/ruby/feature.rb", - "a_mode": null, - "b_mode": null, - "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end", - "new_file": true, - "renamed_file": false, - "deleted_file": false - }] + }], + "compare_timeout": false, + "compare_same_ref": false } ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index eead8b18ebd46..4a9220a7f49bd 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -210,6 +210,12 @@ class Compare < Grape::Entity expose :diffs, using: Entities::RepoDiff do |compare, options| compare.diffs end + + expose :compare_timeout do |compare, options| + compare.timeout + end + + expose :same, as: :compare_same_ref end end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index d59c25cf3167d..a587d4a7bdffa 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -130,8 +130,8 @@ def handle_project_member_errors(errors) # GET /projects/:id/repository/compare?from=master&to=feature get ':id/repository/compare' do authorize! :download_code, user_project + required_attributes! [:from, :to] compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to], MergeRequestDiff::COMMITS_SAFE_SIZE) - present compare, with: Entities::Compare end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 690a70b0b1612..eaf217f0af49d 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -118,18 +118,21 @@ get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'simple_merge_request' response.status.should == 200 json_response['commits'].size.should == 3 + json_response['diffs'].size.should == 1 end it "should compare 2 commits" do get api("/projects/#{project.id}/repository/compare", user), from: 'b1e6a9dbf1c85', to: '1e689bfba395' response.status.should == 200 json_response['commits'].size.should == 0 + json_response['diffs'].size.should == 0 end it "should compare 2 commits" do get api("/projects/#{project.id}/repository/compare", user), from: '1e689bfba395', to: 'b1e6a9dbf1c85' response.status.should == 200 json_response['commits'].size.should == 4 + json_response['diffs'].size.should == 9 end end end From c7e00aca2d68a15c901506f1af4242df92670b6a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 May 2014 11:27:42 +0300 Subject: [PATCH 57/68] Better specs for Compare API Signed-off-by: Dmitriy Zaporozhets --- lib/api/entities.rb | 4 ++- spec/requests/api/repositories_spec.rb | 34 +++++++++++++++++++------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4a9220a7f49bd..6bad6c74bca4c 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -202,7 +202,9 @@ class RepoDiff < Grape::Entity class Compare < Grape::Entity expose :commit, using: Entities::RepoCommit do |compare, options| - Commit.new compare.commit + if compare.commit + Commit.new compare.commit + end end expose :commits, using: Entities::RepoCommit do |compare, options| Commit.decorate compare.commits diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index eaf217f0af49d..a902a1542ccf2 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -114,25 +114,41 @@ end describe 'GET /GET /projects/:id/repository/compare' do - it "should compare 2 branches" do + it "should compare branches" do get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'simple_merge_request' response.status.should == 200 - json_response['commits'].size.should == 3 - json_response['diffs'].size.should == 1 + json_response['commits'].should be_present + json_response['diffs'].should be_present end - it "should compare 2 commits" do + it "should compare tags" do + get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.1', to: 'v1.0.2' + response.status.should == 200 + json_response['commits'].should be_present + json_response['diffs'].should be_present + end + + it "should compare commits" do get api("/projects/#{project.id}/repository/compare", user), from: 'b1e6a9dbf1c85', to: '1e689bfba395' response.status.should == 200 - json_response['commits'].size.should == 0 - json_response['diffs'].size.should == 0 + json_response['commits'].should be_empty + json_response['diffs'].should be_empty + json_response['compare_same_ref'].should be_false end - it "should compare 2 commits" do + it "should compare commits in reverse order" do get api("/projects/#{project.id}/repository/compare", user), from: '1e689bfba395', to: 'b1e6a9dbf1c85' response.status.should == 200 - json_response['commits'].size.should == 4 - json_response['diffs'].size.should == 9 + json_response['commits'].should be_present + json_response['diffs'].should be_present + end + + it "should compare same refs" do + get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master' + response.status.should == 200 + json_response['commits'].should be_empty + json_response['diffs'].should be_empty + json_response['compare_same_ref'].should be_true end end end From d7b3bbfc4ab9d128f0d9c4734b3a7bebbc5d6eef Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 May 2014 12:06:31 +0300 Subject: [PATCH 58/68] Make MR dicussion and changes tabs more noticable Signed-off-by: Dmitriy Zaporozhets --- .../stylesheets/sections/merge_requests.scss | 15 +++++++++++---- app/views/projects/merge_requests/_show.html.haml | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 2d9a5e4bbe6b0..cac4cb66aa415 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -19,12 +19,19 @@ } } -.merge-request .nav-tabs{ +.merge-request .merge-request-tabs{ + border-color: #AAA; li { a { - font-weight: bold; - padding: 8px 20px; - text-align: center; + border-color: #AAA; + padding: 14px 40px; + font-size: 14px; + background-color: #F9F9F9; + } + &.active a { + border-color: #AAA; + border-bottom-color: #FFF; + background-color: #FFF; } } } diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0166b988e580e..cbb8c83e4d056 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -12,10 +12,12 @@ = link_to project_merge_request_path(@project, @merge_request) do %i.icon-comment Discussion + %span.badge= @merge_request.mr_and_commit_notes.count %li.diffs-tab{data: {action: 'diffs'}} = link_to diffs_project_merge_request_path(@project, @merge_request) do %i.icon-list-alt Changes + %span.badge= @merge_request.diffs.size - content_for :note_actions do - if can?(current_user, :modify_merge_request, @merge_request) From f22c709539d06be6fa76f148dedffdef1170936c Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 27 May 2014 14:12:15 +0200 Subject: [PATCH 59/68] Add titles to doc pages. --- doc/api/deploy_keys.md | 2 +- doc/api/groups.md | 2 ++ doc/api/issues.md | 2 ++ doc/api/merge_requests.md | 2 ++ doc/api/milestones.md | 2 ++ doc/api/notes.md | 2 ++ doc/api/project_snippets.md | 2 ++ doc/api/projects.md | 2 +- doc/api/repositories.md | 2 ++ doc/api/repository_files.md | 4 +++- doc/api/session.md | 2 ++ doc/api/system_hooks.md | 2 ++ doc/api/users.md | 2 ++ doc/install/database_mysql.md | 2 ++ doc/install/installation.md | 2 ++ doc/install/requirements.md | 4 +++- doc/legal/corporate_contributor_license_agreement.md | 2 ++ doc/legal/individual_contributor_license_agreement.md | 2 ++ doc/markdown/markdown.md | 2 ++ doc/permissions/permissions.md | 2 ++ doc/public_access/public_access.md | 2 ++ doc/raketasks/backup_restore.md | 2 ++ doc/raketasks/cleanup.md | 2 ++ doc/raketasks/import.md | 2 ++ doc/raketasks/maintenance.md | 2 ++ doc/raketasks/user_management.md | 2 ++ doc/raketasks/web_hooks.md | 2 ++ doc/release/monthly.md | 1 + doc/release/patch.md | 1 + doc/release/security.md | 1 + doc/security/rack_attack.md | 4 +++- doc/ssh/deploy_keys.md | 2 ++ doc/ssh/ssh.md | 2 ++ doc/system_hooks/system_hooks.md | 2 ++ doc/web_hooks/web_hooks.md | 2 ++ doc/workflow/authorization_for_merge_requests.md | 2 ++ doc/workflow/project_features.md | 2 ++ doc/workflow/workflow.md | 2 ++ 38 files changed, 74 insertions(+), 5 deletions(-) diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index d6c0e624dfb00..6aa7be93c01d6 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -1,4 +1,4 @@ -## Deploy Keys +# Deploy Keys ### List deploy keys diff --git a/doc/api/groups.md b/doc/api/groups.md index f5f5d7690509c..1dbb93f9082d3 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1,3 +1,5 @@ +# Groups + ## List project groups Get a list of groups. (As user: my groups, as admin: all groups) diff --git a/doc/api/issues.md b/doc/api/issues.md index d18506f9ce6a6..c769d7bb69abd 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -1,3 +1,5 @@ +# Issues + ## List issues Get all issues created by authenticated user. This function takes pagination parameters diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index d68f34971f1be..284c2befe6fff 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1,3 +1,5 @@ +# Merge requests + ## List merge requests Get all merge requests for this project. diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 2a2ef4b79b1d2..b0f355b9a0c88 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -1,3 +1,5 @@ +# Milestones + ## List project milestones Returns a list of project milestones. diff --git a/doc/api/notes.md b/doc/api/notes.md index e9ad6e00c7316..6d140643fcbc6 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -1,3 +1,5 @@ +# Notes + Notes can be wall notes or comments on snippets, issues or merge requests. ## Wall diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index e16e1e845963e..7a49827233422 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -1,3 +1,5 @@ +# Project snippets + ## List snippets Get a list of project snippets. diff --git a/doc/api/projects.md b/doc/api/projects.md index ffaba0af7fe40..ae2b8365e80fc 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1,4 +1,4 @@ -## Projects +# Projects ### List projects diff --git a/doc/api/repositories.md b/doc/api/repositories.md index e9120e17bb82d..ecb0fa3f6a0d1 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -1,3 +1,5 @@ +# Repositories + ## List project repository tags Get a list of repository tags from a project, sorted by name in reverse alphabetical order. diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index b215cc2500197..820ae71361dfa 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -1,4 +1,6 @@ -# CRUD for repository files +# Repository files + +## CRUD for repository files ## Create, read, update and delete repository files using this API diff --git a/doc/api/session.md b/doc/api/session.md index 0be5af79dad92..2e717a2ea7799 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -1,3 +1,5 @@ +# Session + Login to get private token ``` diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index d4c45ea9bbd9f..0d33aee21339f 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -1,3 +1,5 @@ +# System hooks + All methods require admin authorization. The url endpoint of the system hooks can be configured in [the admin area under hooks](/admin/hooks). diff --git a/doc/api/users.md b/doc/api/users.md index 2b927c3077734..c185cf6478a3e 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -1,3 +1,5 @@ +# Users + ## List users Get a list of users. diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index bf8183729e7fb..270ad3b0b86b2 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -1,3 +1,5 @@ +# Database Mysql + ## Note We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](http://bugs.mysql.com/bug.php?id=65830) that [suggested](http://bugs.mysql.com/bug.php?id=50909) [fixes](http://bugs.mysql.com/bug.php?id=65830) [have](http://bugs.mysql.com/bug.php?id=63164). diff --git a/doc/install/installation.md b/doc/install/installation.md index 44f5a28fde5e1..73fef0dadfeba 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -1,3 +1,5 @@ +# Installation + # 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 branch (version) of GitLab you would like to install. In most cases this should be the highest numbered stable branch (example shown below). diff --git a/doc/install/requirements.md b/doc/install/requirements.md index fd2dd16cd8eb4..2830a75df52bb 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -1,4 +1,6 @@ -# Operating Systems +# Requirements + +## Operating Systems GitLab is developed for the Linux operating system. For the installations options and instructions please see [the installation section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation). diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md index fb8d52e6bd41f..eb808d8a761fd 100644 --- a/doc/legal/corporate_contributor_license_agreement.md +++ b/doc/legal/corporate_contributor_license_agreement.md @@ -1,3 +1,5 @@ +# Corporate contributor license agreement + You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions. 1. Definitions. diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md index 7ac9d6e4cde78..95cbed7e75bb7 100644 --- a/doc/legal/individual_contributor_license_agreement.md +++ b/doc/legal/individual_contributor_license_agreement.md @@ -1,3 +1,5 @@ +# Individual contributor license agreement + You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions. 1. Definitions. diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index e7ebc613431c6..47cb04cdb0484 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -1,3 +1,5 @@ +# Markdown + ---------------------------------------------- Table of Contents diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 9a3dcefe5698f..840bb90163b1e 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -1,3 +1,5 @@ +# Permissions + Users have different abilities depending on the access level they have in a particular group or project. If a user is both in a project group and in the project itself, the highest permission level is used. If a user is a GitLab administrator they receive all permissions. diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index bb23a4bfd96fa..1714a7eeae48b 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -1,3 +1,5 @@ +# Public access + Gitlab allows you to open selected projects to be accessed **publicly** or **internally**. Projects with either of these visibility levels will be listen in the [public access directory](/public). Internal projects will only be available to authenticated users. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index bdff6ad5da8f0..f0be2b6a44187 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -1,3 +1,5 @@ +# Backup restore + ### Create a backup of the GitLab system Creates a backup archive of the database and all repositories. This archive will be saved in backup_path (see `config/gitlab.yml`). diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index 99809ef434df7..b0b82754da688 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -1,3 +1,5 @@ +# Cleanup + ### Remove garbage from filesystem. Important! Data loss! Remove namespaces(dirs) from `/home/git/repositories` if they don't exist in GitLab database. diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md index e11328dc5ce7d..628bd373b8d7c 100644 --- a/doc/raketasks/import.md +++ b/doc/raketasks/import.md @@ -1,3 +1,5 @@ +# Import + ### Import bare repositories into GitLab project instance Notes: diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 907c9352c5976..da58962499b71 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -1,3 +1,5 @@ +# Maintenance + ### Gather information about GitLab and the system it runs on This command gathers information about your GitLab installation and the System diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index e82320829166a..d5b173fde65b1 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -1,3 +1,5 @@ +# User management + ### Add user as a developer to all projects ```bash diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md index 1ca5bacb9d104..4ffbf5f8698a6 100644 --- a/doc/raketasks/web_hooks.md +++ b/doc/raketasks/web_hooks.md @@ -1,3 +1,5 @@ +# Web hooks + ### Add a web hook for **ALL** projects: RAILS_ENV=production bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 5e85fe0825ef7..71cd56a099983 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -1,4 +1,5 @@ # Monthly Release + NOTE: This is a guide for GitLab developers. # **15th - Code Freeze & Release Manager** diff --git a/doc/release/patch.md b/doc/release/patch.md index 30bb39b4e49d6..6d8866a0cfbe7 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -1,4 +1,5 @@ # Things to do when doing a patch release + NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update). ## When to do a patch release diff --git a/doc/release/security.md b/doc/release/security.md index 2fe0a948ad2fe..ac2f79dfb9284 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -1,4 +1,5 @@ # Things to do when doing an out-of-bound security release + NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update). ## When to do a security release diff --git a/doc/security/rack_attack.md b/doc/security/rack_attack.md index a0d02b1650f3e..9e863bbd19018 100644 --- a/doc/security/rack_attack.md +++ b/doc/security/rack_attack.md @@ -1,3 +1,5 @@ +# Rack attack + To prevent abusive clients doing damage GitLab uses rack-attack gem. If you installed or upgraded GitLab by following the official guides this should be enabled by default. If you are missing `config/initializers/rack_attack.rb` the following steps need to be taken in order to enable protection for your GitLab instance: @@ -16,4 +18,4 @@ If you want more restrictive/relaxed throttle rule change the `limit` or `period In case you find throttling is not enough to protect you against abusive clients, rack-attack gem offers IP whitelisting, blacklisting, Fail2ban style filter and tracking. -For more information on how to use these options check out [rack-attack README](https://github.com/kickstarter/rack-attack/blob/master/README.md). \ No newline at end of file +For more information on how to use these options check out [rack-attack README](https://github.com/kickstarter/rack-attack/blob/master/README.md). diff --git a/doc/ssh/deploy_keys.md b/doc/ssh/deploy_keys.md index c7125b7949e7b..e113160c9bc1e 100644 --- a/doc/ssh/deploy_keys.md +++ b/doc/ssh/deploy_keys.md @@ -1,3 +1,5 @@ +# Deploy keys + Deploy keys allow read-only access one or multiple projects with a single SSH key. This is really useful for cloning repositories to your Continuous Integration (CI) server. diff --git a/doc/ssh/ssh.md b/doc/ssh/ssh.md index 0a38bc16b49ce..f89b6a14ce1c4 100644 --- a/doc/ssh/ssh.md +++ b/doc/ssh/ssh.md @@ -1,3 +1,5 @@ +# SSH keys + SSH key allows you to establish a secure connection between your computer and GitLab diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 5c8daf466ab43..5a1a32c1edbd0 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -1,3 +1,5 @@ +# System hooks + Your GitLab instance can perform HTTP POST requests on the following events: `create_project`, `delete_project`, `create_user`, `delete_user` and `change_team_member`. System hooks can be used, e.g. for logging or changing information in a LDAP server. diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 4c06bc4d44401..19a60db00ad34 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -1,3 +1,5 @@ +# Web hooks + Project web hooks allow you to trigger an URL if new code is pushed or a new issue is created. --- diff --git a/doc/workflow/authorization_for_merge_requests.md b/doc/workflow/authorization_for_merge_requests.md index 4e07d7c04c5dc..cc7031b11e143 100644 --- a/doc/workflow/authorization_for_merge_requests.md +++ b/doc/workflow/authorization_for_merge_requests.md @@ -1,3 +1,5 @@ +# Authorization for Merge requests + There are two main ways to have a merge request flow with GitLab: working with protected branches in a single repository, or working with forks of an authoritative project. ## Protected branch flow diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md index 64f4ddaa1255a..ec2c273db01ee 100644 --- a/doc/workflow/project_features.md +++ b/doc/workflow/project_features.md @@ -1,3 +1,5 @@ +# Project features + When in a Project -> Settings, you will find Features on the bottom of the page that you can toggle. Below you will find a more elaborate explanation of each of these. diff --git a/doc/workflow/workflow.md b/doc/workflow/workflow.md index bb232e9d5c57d..8186cd53b200f 100644 --- a/doc/workflow/workflow.md +++ b/doc/workflow/workflow.md @@ -1,3 +1,5 @@ +# Workflow + 1. Clone project ```bash From 046647a74689f8bce86b119c63fa49dfc0756639 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 27 May 2014 14:25:34 +0200 Subject: [PATCH 60/68] Add directions on how to upgrade gitlab-shell in the upgrader guide. --- doc/update/upgrader.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index 72a94f67b3c5b..f8d2d9e749cba 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -34,13 +34,23 @@ __GitLab Upgrader is available only for GitLab version 6.4.2 or higher__ 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! +### (optional) 5. Application status check fails + +If `gitlab:check` task reports old version of gitlab-shell and recommends upgrading, upgrade gitlab-shell by running: + +``` +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v1.9.4 + +``` ### One line upgrade command From c8b58f0007ae81f39534e1de4f8958b9a7ed7590 Mon Sep 17 00:00:00 2001 From: dosire Date: Tue, 27 May 2014 14:56:52 +0200 Subject: [PATCH 61/68] Cherry-pick the changelog additions into master. --- doc/release/patch.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/release/patch.md b/doc/release/patch.md index 30bb39b4e49d6..bd0f16d2b5276 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -9,17 +9,20 @@ Otherwise include it in the monthly release and note there was a regression fix ## Release Procedure 1. Verify that the issue can be repoduced +1. Note in the 'GitLab X.X regressions' that you will create a patch 1. Create an issue on private GitLab development server 1. Name the issue "Release X.X.X CE and X.X.X EE", this will make searching easier 1. Fix the issue on a feature branch, do this on the private GitLab development server 1. Consider creating and testing workarounds 1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch -1. In a separate commit in the stable branch, update the VERSION and CHANGELOG +1. In a separate commit in the stable branch update the VERSION +1. In a separate commit in the stable branch update the CHANGELOG 1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X" 1. Create an annotated tag vX.X.X for CE and another patch release for EE -1. Make sure that the build has passed and no tests are failing +1. Make sure that the build has passed and all tests are passing 1. Push the code and the tags to all the CE and EE repositories 1. Apply the patch to GitLab Cloud and the private GitLab development server -1. Send tweets about the release from @gitlabhq, tweet should include the most important feature that the release is addressing as well as the link to the changelog 1. Build new packages with the latest version - +1. Cherry-pick the changelog update back into master +1. Send tweets about the release from @gitlabhq, tweet should include the most important feature that the release is addressing as well as the link to the changelog +1. Note in the 'GitLab X.X regressions' issue that the patch was published From 7dfb678df98b1b9ad420d9cbf4fe74f1553f34a7 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 27 May 2014 15:45:24 +0200 Subject: [PATCH 62/68] Revert "Actually use the 'user_filter' configuration option" This reverts commit e9d4587ff11c8510f01dfa184414f73d75b4550b, which is incompatible with GitLab's built in LDAP user filter: a GitLab LDAP filter is [added on top of the other filters used to find the user](https://gitlab.com/gitlab-org/gitlab-ce/blob/982d4d51e8110bec280eb00db0fb756b062103d9/lib/gitlab/ldap/adapter.rb#L61) Example GitLab LDAP filter: `(memberOf=cn=foo,dc=bar)`. In contrast, an omniauth-ldap filter [replaces the 'normal' filters](https://gitlab.com/gitlab-org/omniauth-ldap/blob/76d77543dec0c585bb4e974262f43675f9810953/lib/omniauth/strategies/ldap.rb#L55) Example omniauth-ldap user filter: `(&(uid=%{username})(memberOf=cn=foo,dc=bar))`. --- config/initializers/devise.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index d5cb110e8814f..50669ece7a81a 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -223,7 +223,6 @@ method: Gitlab.config.ldap['method'], bind_dn: Gitlab.config.ldap['bind_dn'], password: Gitlab.config.ldap['password'], - filter: Gitlab.config.ldap['user_filter'], name_proc: email_stripping_proc end @@ -245,4 +244,4 @@ config.omniauth provider['name'].to_sym, *provider_arguments end -end \ No newline at end of file +end From cee88509a382d93468982d33bee33a2dd08eb389 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 27 May 2014 16:30:00 +0200 Subject: [PATCH 63/68] Fix typo in text. --- doc/update/upgrader.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index f8d2d9e749cba..0a1ad099daced 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -43,13 +43,12 @@ If all items are green, then congratulations upgrade is complete! ### (optional) 5. Application status check fails -If `gitlab:check` task reports old version of gitlab-shell and recommends upgrading, upgrade gitlab-shell by running: +If the `gitlab:check` task reports an old version of gitlab-shell and recommends upgrading, upgrade gitlab-shell by running: ``` cd /home/git/gitlab-shell sudo -u git -H git fetch sudo -u git -H git checkout v1.9.4 - ``` ### One line upgrade command From 2341cefd6fa2811a1af41f2068554738d7eebfc4 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 27 May 2014 17:14:41 +0200 Subject: [PATCH 64/68] Move from script to bin directory. --- README.md | 2 +- {script => bin}/background_jobs | 0 {script => bin}/check | 0 {script => bin}/upgrade.rb | 0 {script => bin}/web | 0 doc/update/upgrader.md | 6 +++--- lib/support/init.d/gitlab | 12 ++++++------ lib/tasks/gitlab/check.rake | 2 +- lib/tasks/sidekiq.rake | 8 ++++---- script/rails | 6 ------ 10 files changed, 15 insertions(+), 21 deletions(-) rename {script => bin}/background_jobs (100%) rename {script => bin}/check (100%) rename {script => bin}/upgrade.rb (100%) rename {script => bin}/web (100%) delete mode 100755 script/rails diff --git a/README.md b/README.md index 283336b9e4d8c..256f640454edf 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Start it with [Foreman](https://github.com/ddollar/foreman) or start each component separately bundle exec rails s - script/background_jobs start + bin/background_jobs start And surf to [localhost:3000](http://localhost:3000/) and login with root / 5iveL!fe diff --git a/script/background_jobs b/bin/background_jobs similarity index 100% rename from script/background_jobs rename to bin/background_jobs diff --git a/script/check b/bin/check similarity index 100% rename from script/check rename to bin/check diff --git a/script/upgrade.rb b/bin/upgrade.rb similarity index 100% rename from script/upgrade.rb rename to bin/upgrade.rb diff --git a/script/web b/bin/web similarity index 100% rename from script/web rename to bin/web diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index 72a94f67b3c5b..d08c8b85b5ffa 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -19,10 +19,10 @@ __GitLab Upgrader is available only for GitLab version 6.4.2 or higher__ ### 2. Run gitlab upgrade tool cd /home/git/gitlab - sudo -u git -H ruby script/upgrade.rb + sudo -u git -H ruby bin/upgrade.rb # to perform a non-interactive install (no user input required) you can add -y - # sudo -u git -H ruby script/upgrade.rb -y + # sudo -u git -H ruby bin/upgrade.rb -y ### 3. Start application @@ -48,6 +48,6 @@ You've read through the entire guide, and probably did all the steps manually. H ```bash cd /home/git/gitlab; sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; \ - sudo service gitlab stop; sudo -u git -H ruby script/upgrade.rb -y; sudo service gitlab start; \ + sudo service gitlab stop; sudo -u git -H ruby bin/upgrade.rb -y; sudo service gitlab start; \ sudo service nginx restart; sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production ``` diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 3dd4465a6d8a0..5c1ce2dadab86 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -167,14 +167,14 @@ start_gitlab() { # Remove old socket if it exists rm -f "$socket_path"/gitlab.socket 2>/dev/null # Start the web server - RAILS_ENV=$RAILS_ENV script/web start + RAILS_ENV=$RAILS_ENV bin/web start fi # If sidekiq is already running, don't start it again. if [ "$sidekiq_status" = "0" ]; then echo "The Sidekiq job dispatcher is already running with pid $spid, not restarting" else - RAILS_ENV=$RAILS_ENV script/background_jobs start & + RAILS_ENV=$RAILS_ENV bin/background_jobs start & fi # Wait for the pids to be planted @@ -197,11 +197,11 @@ stop_gitlab() { # If the Unicorn web server is running, tell it to stop; if [ "$web_status" = "0" ]; then - RAILS_ENV=$RAILS_ENV script/web stop + RAILS_ENV=$RAILS_ENV bin/web stop fi # And do the same thing for the Sidekiq. if [ "$sidekiq_status" = "0" ]; then - RAILS_ENV=$RAILS_ENV script/background_jobs stop + RAILS_ENV=$RAILS_ENV bin/background_jobs stop fi # If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script. @@ -253,10 +253,10 @@ reload_gitlab(){ exit 1 fi printf "Reloading GitLab Unicorn configuration... " - RAILS_ENV=$RAILS_ENV script/web reload + RAILS_ENV=$RAILS_ENV bin/web reload echo "Done." echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..." - RAILS_ENV=$RAILS_ENV script/background_jobs restart + RAILS_ENV=$RAILS_ENV bin/background_jobs restart wait_for_pids print_status diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 0387795fa4846..ad5e04ecab944 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -637,7 +637,7 @@ namespace :gitlab do else puts "no".red try_fixing_it( - sudo_gitlab("RAILS_ENV=production script/background_jobs start") + sudo_gitlab("RAILS_ENV=production bin/background_jobs start") ) for_more_information( see_installation_guide_section("Install Init Script"), diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake index ba806e53ccf12..e4bd6545755f2 100644 --- a/lib/tasks/sidekiq.rake +++ b/lib/tasks/sidekiq.rake @@ -1,21 +1,21 @@ namespace :sidekiq do desc "GITLAB | Stop sidekiq" task :stop do - system *%W(script/background_jobs stop) + system *%W(bin/background_jobs stop) end desc "GITLAB | Start sidekiq" task :start do - system *%W(script/background_jobs start) + system *%W(bin/background_jobs start) end desc 'GitLab | Restart sidekiq' task :restart do - system *%W(script/background_jobs restart) + system *%W(bin/background_jobs restart) end desc "GITLAB | Start sidekiq with launchd on Mac OS X" task :launchd do - system *%W(script/background_jobs start_no_deamonize) + system *%W(bin/background_jobs start_no_deamonize) end end diff --git a/script/rails b/script/rails deleted file mode 100755 index f8da2cffd4de0..0000000000000 --- a/script/rails +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -APP_PATH = File.expand_path('../../config/application', __FILE__) -require File.expand_path('../../config/boot', __FILE__) -require 'rails/commands' From affd376f7a36df4a80369bc1c7c8557dd4822b7d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 May 2014 18:51:41 +0300 Subject: [PATCH 65/68] Dont show remove source branch ckecbox/button if you dont have such permissions for MR page Signed-off-by: Dmitriy Zaporozhets --- app/controllers/projects/merge_requests_controller.rb | 6 ------ app/models/merge_request.rb | 4 ---- app/views/projects/merge_requests/show/_mr_accept.html.haml | 2 +- .../merge_requests/show/_remove_source_branch.html.haml | 2 +- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d8551db7b012c..a0a8aa059ec2f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -225,7 +225,6 @@ def define_show_vars @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 - @allowed_to_remove_source_branch = allowed_to_remove_source_branch? @source_branch = @merge_request.source_project.repository.find_branch(@merge_request.source_branch).try(:name) end @@ -238,11 +237,6 @@ def invalid_mr render 'invalid' end - def allowed_to_remove_source_branch? - allowed_to_push_code?(@merge_request.source_project, @merge_request.source_branch) && - !@merge_request.disallow_source_branch_removal? - end - def allowed_to_push_code?(project, branch) action = if project.protected_branch?(branch) :push_code_to_protected_branches diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 061537132b3fb..b7c4a2a923cfd 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -212,10 +212,6 @@ def for_fork? target_project != source_project end - def disallow_source_branch_removal? - source_project.root_ref?(source_branch) || source_project.protected_branches.include?(source_branch) - end - def project target_project end diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index 1276489c2d9e8..07e05f5501254 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -38,7 +38,7 @@ .accept-group .pull-left = f.submit "Accept Merge Request", class: "btn btn-create accept_merge_request" - - unless @merge_request.disallow_source_branch_removal? + - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) .remove_branch_holder.pull-left = label_tag :should_remove_source_branch, class: "checkbox" do = check_box_tag :should_remove_source_branch 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 index f8f8b71f21d41..491360af1bbff 100644 --- a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml +++ b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml @@ -1,7 +1,7 @@ - if @source_branch.blank? Source branch has been removed -- elsif @allowed_to_remove_source_branch && @merge_request.merged? +- 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 project_branch_path(@merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-small remove_source_branch" do From c53f0766c82e481d985d684fa28d99a706883d55 Mon Sep 17 00:00:00 2001 From: erbunao Date: Mon, 26 May 2014 15:02:52 +0800 Subject: [PATCH 66/68] Implements and refactors clipboard feature for markdown. --- .../javascripts/markdown_area.js.coffee | 108 +++++++++++++++--- app/controllers/projects_controller.rb | 6 + 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/markdown_area.js.coffee b/app/assets/javascripts/markdown_area.js.coffee index def5d12a820f9..84e7b7ae1ccb7 100644 --- a/app/assets/javascripts/markdown_area.js.coffee +++ b/app/assets/javascripts/markdown_area.js.coffee @@ -1,6 +1,3 @@ -formatLink = (str) -> - "![" + str.alt + "](" + str.url + ")" - $(document).ready -> alertClass = "alert alert-danger alert-dismissable div-dropzone-alert" alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"" @@ -13,15 +10,12 @@ $(document).ready -> project_image_path_upload = window.project_image_path_upload or null $("textarea.markdown-area").wrap "
" - $(".div-dropzone").parent().addClass "div-dropzone-wrapper" - $(".div-dropzone").append divHover $(".div-dropzone-hover").append iconPicture $(".div-dropzone").append divSpinner $(".div-dropzone-spinner").append iconSpinner - dropzone = $(".div-dropzone").dropzone( url: project_image_path_upload dictDefaultMessage: "" @@ -36,7 +30,7 @@ $(document).ready -> previewContainer: false processing: -> - $(".div-dropzone-alert").alert "close" + closeAlertMessage() dragover: -> $(".div-dropzone > textarea").addClass "div-dropzone-focus" @@ -55,31 +49,115 @@ $(document).ready -> return success: (header, response) -> - child = $(dropzone[0]).children("textarea") - $(child).val $(child).val() + formatLink(response.link) + "\n" + appendToTextArea(formatLink(response.link)) return error: (temp, errorMessage) -> - checkIfMsgExists = $(".error-alert").children().length - if checkIfMsgExists is 0 - $(".error-alert").append divAlert - $(".div-dropzone-alert").append btnAlert + errorMessage + showError(errorMessage) return sending: -> - $(".div-dropzone-spinner").css "opacity", 0.7 + showSpinner() return complete: -> $(".dz-preview").remove() $(".markdown-area").trigger "input" - $(".div-dropzone-spinner").css "opacity", 0 + closeSpinner() return ) + child = $(dropzone[0]).children("textarea") + + formatLink = (str) -> + "![" + str.alt + "](" + str.url + ")" + + handlePaste = (e) -> + e.preventDefault() + my_event = e.originalEvent + filename = getFilename(my_event) + appendFilename(filename) + + i = 0 + while i < my_event.clipboardData.items.length + item = my_event.clipboardData.items[i] + if item.type.indexOf("image") isnt -1 + uploadFile item.getAsFile(), filename + i++ + return + + appendFilename = (filename) -> + caretStart = $(child)[0].selectionStart + caretEnd = $(child)[0].selectionEnd + textEnd = $(child).val().length + + beforeSelection = $(child).val().substring 0, caretStart + afterSelection = $(child).val().substring caretEnd, textEnd + $(child).val beforeSelection + "{{" + filename + "}}" + afterSelection + + getFilename = (e) -> + if window.clipboardData and window.clipboardData.getData + value = window.clipboardData.getData("Text") + else if e.clipboardData and e.clipboardData.getData + value = e.clipboardData.getData("text") + value = value.split("\r") + return value.first() + + uploadFile = (item, filename) -> + formData = new FormData() + formData.append "markdown_img", item, filename + + $.ajax + url: project_image_path_upload + type: "POST" + data: formData + dataType: "json" + processData: false + contentType: false + headers: + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + + beforeSend: -> + showSpinner() + closeAlertMessage() + + success: (e, textStatus, response) -> + insertToTextArea(filename, formatLink(response.responseJSON.link)) + + error: (response) -> + showError(response.responseJSON.message) + + complete: -> + closeSpinner() + + insertToTextArea = (filename, url) -> + $(child).val (index, val) -> + val.replace("{{" + filename + "}}", url + "\n") + + appendToTextArea = (url) -> + $(child).val (index, val) -> + val + url + "\n" + + showSpinner = (e) -> + $(".div-dropzone-spinner").css "opacity", 0.7 + + closeSpinner = -> + $(".div-dropzone-spinner").css "opacity", 0 + + showError = (message) -> + checkIfMsgExists = $(".error-alert").children().length + if checkIfMsgExists is 0 + $(".error-alert").append divAlert + $(".div-dropzone-alert").append btnAlert + message + + closeAlertMessage = -> + $(".div-dropzone-alert").alert "close" + $(".markdown-selector").click (e) -> e.preventDefault() $(".div-dropzone").click() return + $(".div-dropzone").on "paste", handlePaste + return \ No newline at end of file diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 07ccbd57fafb6..54c7c6bc52eeb 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -11,6 +11,8 @@ class ProjectsController < ApplicationController layout 'navless', only: [:new, :create, :fork] before_filter :set_title, only: [:new, :create] + rescue_from CarrierWave::IntegrityError, :with => :invalid_file + def new @project = Project.new end @@ -185,6 +187,10 @@ def accepted_images %w(png jpg jpeg gif) end + def invalid_file(error) + render json: { message: error.message }, status: :internal_server_error + end + def set_title @title = 'New Project' end From 1df3c9b29acdeaf2e230d173d20a1051b8eaa81b Mon Sep 17 00:00:00 2001 From: erbunao Date: Tue, 27 May 2014 16:52:29 +0800 Subject: [PATCH 67/68] minor fix --- app/assets/javascripts/markdown_area.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/markdown_area.js.coffee b/app/assets/javascripts/markdown_area.js.coffee index 84e7b7ae1ccb7..61c9e8b5d3505 100644 --- a/app/assets/javascripts/markdown_area.js.coffee +++ b/app/assets/javascripts/markdown_area.js.coffee @@ -75,7 +75,7 @@ $(document).ready -> handlePaste = (e) -> e.preventDefault() my_event = e.originalEvent - filename = getFilename(my_event) + filename = getFilename(my_event) or "image.png" appendFilename(filename) i = 0 @@ -106,7 +106,7 @@ $(document).ready -> uploadFile = (item, filename) -> formData = new FormData() formData.append "markdown_img", item, filename - + console.log filename $.ajax url: project_image_path_upload type: "POST" From 331c2501d57e404f192da137839829a16937cd9d Mon Sep 17 00:00:00 2001 From: erbunao Date: Wed, 28 May 2014 11:37:01 +0800 Subject: [PATCH 68/68] wdlakjlds --- .../javascripts/markdown_area.js.coffee | 47 ++++++++++++------- .../stylesheets/generic/markdown_area.scss | 5 ++ app/views/projects/issues/_form.html.haml | 2 +- .../projects/merge_requests/_form.html.haml | 3 +- .../merge_requests/_new_submit.html.haml | 3 +- app/views/projects/milestones/_form.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 6 +-- app/views/projects/wikis/_form.html.haml | 3 +- 8 files changed, 47 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/markdown_area.js.coffee b/app/assets/javascripts/markdown_area.js.coffee index 61c9e8b5d3505..63561dc4b2087 100644 --- a/app/assets/javascripts/markdown_area.js.coffee +++ b/app/assets/javascripts/markdown_area.js.coffee @@ -75,38 +75,53 @@ $(document).ready -> handlePaste = (e) -> e.preventDefault() my_event = e.originalEvent - filename = getFilename(my_event) or "image.png" - appendFilename(filename) - - i = 0 - while i < my_event.clipboardData.items.length - item = my_event.clipboardData.items[i] - if item.type.indexOf("image") isnt -1 - uploadFile item.getAsFile(), filename - i++ - return - - appendFilename = (filename) -> + + if my_event.clipboardData and my_event.clipboardData.items + i = 0 + console.log my_event.clipboardData.items[0] + while i < my_event.clipboardData.items.length + item = my_event.clipboardData.items[i] + processItem(my_event, item) + i++ + + processItem = (e, item) -> + console.log e.clipboardData.items.length + console.log isImage(item) + if isImage(item) + filename = getFilename(e) or "image.png" + text = "{{" + filename + "}}" + pasteText(text) + uploadFile item.getAsFile(), filename + else if e.clipboardData.items.length == 1 + text = e.clipboardData.getData("text/plain") + pasteText(text) + + isImage = (item) -> + if item + item.type.indexOf("image") isnt -1 + + pasteText = (text) -> caretStart = $(child)[0].selectionStart caretEnd = $(child)[0].selectionEnd textEnd = $(child).val().length beforeSelection = $(child).val().substring 0, caretStart afterSelection = $(child).val().substring caretEnd, textEnd - $(child).val beforeSelection + "{{" + filename + "}}" + afterSelection + $(child).val beforeSelection + text + afterSelection + $(".markdown-area").trigger "input" getFilename = (e) -> if window.clipboardData and window.clipboardData.getData value = window.clipboardData.getData("Text") else if e.clipboardData and e.clipboardData.getData - value = e.clipboardData.getData("text") + value = e.clipboardData.getData("text/plain") + value = value.split("\r") - return value.first() + value.first() uploadFile = (item, filename) -> formData = new FormData() formData.append "markdown_img", item, filename - console.log filename $.ajax url: project_image_path_upload type: "POST" diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index fbfa72c5e5e92..ae7ace61d49c4 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -43,6 +43,11 @@ display: none; } } + + .hint { + padding: 0; + margin: 0; + } } .div-dropzone-alert { diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 49d1a87743f13..6e61fc8524f22 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -23,7 +23,7 @@ = f.text_area :description, class: 'form-control js-gfm-input markdown-area', rows: 14 .col-sm-12.hint .pull-left Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping, #{link_to "selecting them", '#', class: 'markdown-selector' } or pasting from the clipboard. .clearfix .error-alert %hr diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 3088f0cf864a3..facc6a63f3b05 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -25,7 +25,8 @@ = f.text_area :description, class: "form-control js-gfm-input markdown-area", rows: 14 .col-sm-12.hint .pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping, #{link_to "selecting them", '#', class: 'markdown-selector' } or pasting from the clipboard. + .clearfix .error-alert %hr diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index dc41ef8135e08..bf0e40f9a1fbb 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -26,7 +26,8 @@ = f.text_area :description, class: "form-control js-gfm-input markdown-area", rows: 10 .col-sm-12.hint .pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping, #{link_to "selecting them", '#', class: 'markdown-selector' } or pasting from the clipboard. + .clearfix .error-alert .form-group diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 7e0fd19d0abfd..2233a8838c32e 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -24,7 +24,7 @@ = f.text_area :description, maxlength: 2000, class: "form-control markdown-area", rows: 10 .hint .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - .pull-left Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .pull-left Attach images (JPG, PNG, GIF) by dragging & dropping, #{link_to "selecting them", '#', class: 'markdown-selector' } or pasting from the clipboard. .clearfix .error-alert .col-md-6 diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 2e84dcea899bb..25ebaca930524 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -16,10 +16,10 @@ .note-write-holder = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input markdown-area' - .light.clearfix + .light.clearfix.hint .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. - + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping, #{link_to "selecting them", '#', class: 'markdown-selector' } or pasting from the clipboard. + .error-alert .note-preview-holder.hide .js-note-preview diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index a43e6f073e155..303cec8e98209 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -25,7 +25,8 @@ = f.text_area :content, class: 'form-control js-gfm-input markdown-area', rows: 18 .col-sm-12.hint .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping, #{link_to "selecting them", '#', class: 'markdown-selector' } or pasting from the clipboard. + .clearfix .error-alert .form-group