summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouwe Maan <douwe@gitlab.com>2015-10-27 15:06:40 +0100
committerDouwe Maan <douwe@gitlab.com>2015-10-27 15:06:40 +0100
commit740feeec772565b0734cae816b31dcb47e5f4492 (patch)
tree600cac255cb3ceeb843ec03c6e84c44168964264
parent7851a292a1fc7da3cd2d1140cd40f35009a9c082 (diff)
parent940d68cc4c349b574166b010666a36cf25f485b7 (diff)
downloadgitlab-ce-740feeec772565b0734cae816b31dcb47e5f4492.tar.gz
Merge branch 'master' into reference-pipeline-and-caching
-rw-r--r--CHANGELOG32
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Black.ttfbin148368 -> 289364 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttfbin0 -> 103404 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Bold.ttfbin291424 -> 291424 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttfbin0 -> 103608 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-ExtraLight.ttfbin150528 -> 291652 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttfbin0 -> 104768 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttfbin0 -> 104236 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Light.ttfbin293220 -> 293220 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttfbin0 -> 104616 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Regular.ttfbin293956 -> 293956 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Semibold.ttfbin292404 -> 292404 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttfbin0 -> 104020 bytes
-rw-r--r--app/assets/javascripts/ci/build.coffee4
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js.coffee21
-rw-r--r--app/assets/stylesheets/framework/buttons.scss11
-rw-r--r--app/assets/stylesheets/framework/mixins.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss21
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss9
-rw-r--r--app/assets/stylesheets/pages/projects.scss35
-rw-r--r--app/assets/stylesheets/pages/runners.scss52
-rw-r--r--app/assets/stylesheets/pages/snippets.scss60
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/ci/admin/runners_controller.rb1
-rw-r--r--app/controllers/ci/application_controller.rb8
-rw-r--r--app/controllers/projects/builds_controller.rb17
-rw-r--r--app/controllers/projects/ci_services_controller.rb8
-rw-r--r--app/controllers/projects/commit_controller.rb11
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb9
-rw-r--r--app/controllers/projects/snippets_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb6
-rw-r--r--app/helpers/ci_status_helper.rb9
-rw-r--r--app/helpers/clipboard_helper.rb8
-rw-r--r--app/helpers/tab_helper.rb18
-rw-r--r--app/models/ci/project.rb1
-rw-r--r--app/models/ci/project_status.rb4
-rw-r--r--app/models/ci/runner.rb1
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/merge_request.rb12
-rw-r--r--app/models/project.rb11
-rw-r--r--app/models/repository.rb22
-rw-r--r--app/models/user.rb23
-rw-r--r--app/services/ci/image_for_build_service.rb16
-rw-r--r--app/services/merge_requests/refresh_service.rb75
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/views/ci/admin/runner_projects/index.html.haml2
-rw-r--r--app/views/ci/admin/runners/index.html.haml4
-rw-r--r--app/views/ci/admin/runners/show.html.haml34
-rw-r--r--app/views/ci/lints/_create.html.haml7
-rw-r--r--app/views/ci/user_sessions/new.html.haml3
-rw-r--r--app/views/dashboard/snippets/index.html.haml26
-rw-r--r--app/views/explore/snippets/index.html.haml3
-rw-r--r--app/views/projects/_readme.html.haml10
-rw-r--r--app/views/projects/builds/show.html.haml4
-rw-r--r--app/views/projects/buttons/_notifications.html.haml4
-rw-r--r--app/views/projects/ci_settings/_no_runners.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml6
-rw-r--r--app/views/projects/empty.html.haml85
-rw-r--r--app/views/projects/issues/_discussion.html.haml6
-rw-r--r--app/views/projects/issues/_issues.html.haml7
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml3
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml6
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml7
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml2
-rw-r--r--app/views/projects/snippets/_actions.html.haml11
-rw-r--r--app/views/projects/snippets/index.html.haml20
-rw-r--r--app/views/projects/snippets/show.html.haml46
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/shared/snippets/_header.html.haml24
-rw-r--r--app/views/shared/snippets/_snippet.html.haml1
-rw-r--r--app/views/snippets/_actions.html.haml11
-rw-r--r--app/views/snippets/show.html.haml51
-rw-r--r--config/gitlab.yml.example6
-rw-r--r--config/initializers/1_settings.rb4
-rw-r--r--db/migrate/20151023112551_fail_build_with_empty_name.rb5
-rw-r--r--db/migrate/20151023144219_remove_satellites.rb17
-rw-r--r--db/migrate/20151026182941_add_project_path_index.rb9
-rw-r--r--db/schema.rb3
-rw-r--r--doc/ci/docker/using_docker_images.md2
-rw-r--r--doc/install/installation.md5
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--features/project/merge_requests.feature6
-rw-r--r--features/project/snippets.feature2
-rw-r--r--features/snippets/snippets.feature2
-rw-r--r--features/steps/project/merge_requests.rb13
-rw-r--r--features/steps/project/snippets.rb8
-rw-r--r--features/steps/snippets/snippets.rb6
-rw-r--r--features/steps/snippets/user.rb6
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/ci/api/api.rb2
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb38
-rw-r--r--lib/ci/migrate/builds.rb29
-rw-r--r--lib/ci/migrate/database.rb67
-rw-r--r--lib/ci/migrate/manager.rb72
-rw-r--r--lib/ci/migrate/tags.rb42
-rw-r--r--lib/gitlab/backend/grack_auth.rb9
-rw-r--r--lib/gitlab/database.rb2
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/tasks/ci/migrate.rake87
-rw-r--r--spec/controllers/projects_controller_spec.rb35
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb14
-rw-r--r--spec/lib/gitlab/backend/grack_auth_spec.rb16
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb6
-rw-r--r--spec/requests/api/api_helpers_spec.rb26
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb5
-rw-r--r--vendor/assets/javascripts/clipboard.js621
113 files changed, 1365 insertions, 729 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 4a9a85d5ebf..ea8c6fb5c17 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,23 +1,40 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.2.0 (unreleased)
- - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu)
- Improved performance of replacing references in comments
- - Fix duplicate repositories in GitHub import page (Stan Hu)
- - Redirect to a default path if HTTP_REFERER is not set (Stan Hu)
- Show last project commit to default branch on project home page
- Highlight comment based on anchor in URL
- Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw)
- Improved performance of sorting milestone issues
- Allow users to select the Files view as default project view (Cristian Bica)
-
-v 8.1.0 (unreleased)
+ - Show "Empty Repository Page" for repository without branches (Artem V. Navrotskiy)
+ - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork
+ - Use git follow flag for commits page when retrieve history for file or directory
+ - Show merge request CI status on merge requests index page
+ - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
+
+v 8.1.1
+ - Fix cloning Wiki repositories via HTTP (Stan Hu)
+ - Add migration to remove satellites directory
+ - Fix specific runners visibility
+ - Fix 500 when editing CI service
+ - Require CI jobs to be named
+ - Fix CSS for runner status
+ - Fix CI badge
+ - Allow developer to manage builds
+
+v 8.1.0
+ - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu)
+ - Fix duplicate repositories in GitHub import page (Stan Hu)
+ - Redirect to a default path if HTTP_REFERER is not set (Stan Hu)
- Send an email to admin email when a user is reported for spam (Jonathan Rochkind)
- Show notifications button when user is member of group rather than project (Grzegorz Bizon)
- Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge.
- Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu)
+ - Don't show "Add README" link in an empty repository if user doesn't have access to push (Stan Hu)
- Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu)
- Speed up load times of issue detail pages by roughly 1.5x
+ - Fix CI rendering regressions
- If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg)
- Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu)
- Make diff file view easier to use on mobile screens (Stan Hu)
@@ -27,8 +44,10 @@ v 8.1.0 (unreleased)
- Allow removing of project without confirmation when JavaScript is disabled (Stan Hu)
- Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters (Stan Hu)
- Improved performance of the trending projects page
+ - Remove CI migration task
- Improved performance of finding projects by their namespace
- Fix bug where transferring a project would result in stale commit links (Stan Hu)
+ - Fix build trace updating
- Include full path of source and target branch names in New Merge Request page (Stan Hu)
- Add user preference to view activities as default dashboard (Stan Hu)
- Add option to admin area to sign in as a specific user (Pavel Forkert)
@@ -71,6 +90,7 @@ v 8.1.0 (unreleased)
- Fix position of hamburger in header for smaller screens (Han Loong Liauw)
- Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji)
- Persist filters when sorting on admin user page (Jerry Lukins)
+ - Update style of snippets pages (Han Loong Liauw)
- Allow dashboard and group issues/MRs to be filtered by label
- Add spellcheck=false to certain input fields
- Invalidate stored service password if the endpoint URL is changed
@@ -83,11 +103,11 @@ v 8.1.0 (unreleased)
- Let gitlab-git-http-server generate and serve 'git archive' downloads
- Optimize query when filtering on issuables (Zeger-Jan van de Weg)
- Fix padding of outdated discussion item.
+ - Animate the logo on hover
v 8.0.5
- Correct lookup-by-email for LDAP logins
- Fix loading spinner sometimes not being hidden on Merge Request tab switches
- - Animate the logo on hover
v 8.0.4
- Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu)
diff --git a/Gemfile b/Gemfile
index 9254ce2ccfa..39ec04951c0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -197,7 +197,7 @@ gem 'bootstrap-sass', '~> 3.0'
gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.1'
gem 'gon', '~> 5.0.0'
-gem 'jquery-atwho-rails', '~> 1.0.0'
+gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 3.1.3'
gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 4.2.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 53122898b07..340eb0fc301 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -354,7 +354,7 @@ GEM
ice_nine (0.11.1)
inflecto (0.0.2)
ipaddress (0.8.0)
- jquery-atwho-rails (1.0.1)
+ jquery-atwho-rails (1.3.2)
jquery-rails (3.1.3)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
@@ -840,7 +840,7 @@ DEPENDENCIES
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
- jquery-atwho-rails (~> 1.0.0)
+ jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 3.1.3)
jquery-scrollto-rails (~> 1.4.3)
jquery-turbolinks (~> 2.0.1)
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf
index cb89a2d171e..9c9b5cb7f03 100755..100644
--- a/app/assets/fonts/SourceSansPro-Black.ttf
+++ b/app/assets/fonts/SourceSansPro-Black.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf b/app/assets/fonts/SourceSansPro-BlackIt.ttf
new file mode 100644
index 00000000000..294ce5abe8f
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf
index 5d65c93242f..5d65c93242f 100755..100644
--- a/app/assets/fonts/SourceSansPro-Bold.ttf
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf b/app/assets/fonts/SourceSansPro-BoldIt.ttf
new file mode 100644
index 00000000000..3decd130070
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
index bb4176c6fff..253eafa3783 100755..100644
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
new file mode 100644
index 00000000000..00d7e9a7aa8
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf
new file mode 100644
index 00000000000..f7af5377595
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-It.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf
index 83a0a336661..83a0a336661 100755..100644
--- a/app/assets/fonts/SourceSansPro-Light.ttf
+++ b/app/assets/fonts/SourceSansPro-Light.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf
new file mode 100644
index 00000000000..f18827985ef
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf
index 44486cdc670..44486cdc670 100755..100644
--- a/app/assets/fonts/SourceSansPro-Regular.ttf
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf
index 86b00c067e0..86b00c067e0 100755..100644
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
new file mode 100644
index 00000000000..13d66a1fc45
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
Binary files differ
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index c30859b484b..44d5ddb7d95 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -22,7 +22,7 @@ class CiBuild
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
- if window.location.href is build_url
+ if window.location.href.split("#").first() is build_url
$.ajax
url: build_url
dataType: "json"
@@ -31,7 +31,7 @@ class CiBuild
$('#build-trace code').html build.trace_html
$('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
@checkAutoscroll()
- else
+ else if build.status != build_status
Turbolinks.visit build_url
, 4000
diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee
new file mode 100644
index 00000000000..ec4b80cca6f
--- /dev/null
+++ b/app/assets/javascripts/copy_to_clipboard.js.coffee
@@ -0,0 +1,21 @@
+#= require clipboard
+
+$ ->
+ clipboard = new Clipboard '.js-clipboard-trigger',
+ text: (trigger) ->
+ $target = $(trigger.nextElementSibling || trigger.previousElementSibling)
+ $target.data('clipboard-text') || $target.text().trim()
+
+ clipboard.on 'success', (e) ->
+ $(e.trigger).
+ tooltip(trigger: 'manual', placement: 'auto bottom', title: 'Copied!').
+ tooltip('show')
+
+ # Clear the selection and blur the trigger so it loses its border
+ e.clearSelection()
+ $(e.trigger).blur()
+
+ # Manually hide the tooltip after 1 second
+ setTimeout(->
+ $(e.trigger).tooltip('hide')
+ , 1000)
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index e5f0c0ad9ef..04024419584 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -162,10 +162,21 @@
border-color: #e7e9ed;
width: 140px;
+ .badge {
+ font-weight: normal;
+ background-color: #eee;
+ color: #78a;
+ }
+
&.active {
border-color: $gl-info;
background: $gl-info;
color: #fff;
+
+ .badge {
+ color: $gl-info;
+ background-color: white;
+ }
}
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 089e6958eeb..fe078d016d7 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -147,14 +147,8 @@
.badge {
font-weight: normal;
- background-color: #fff;
background-color: #eee;
color: #78a;
}
}
}
-
-.fa-align {
- top: 20px;
- position: relative;
-}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 9da085a3473..abc27a19e32 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -80,3 +80,24 @@
}
}
}
+
+.issuable-filter-count {
+ span {
+ display: block;
+ margin-bottom: -16px;
+ padding: 13px 0;
+ }
+}
+
+.cross-project-reference {
+ text-align: center;
+ width: 100%;
+
+ .slead {
+ padding: 5px;
+ }
+
+ span, button {
+ background-color: $background-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index a1a5208c59c..f0b3667acca 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -205,6 +205,15 @@
#modal_merge_info .modal-dialog {
width: 600px;
+
+ .btn-clipboard {
+ @extend .pull-right;
+
+ margin-right: 18px;
+ margin-top: 5px;
+ position: absolute;
+ right: 0;
+ }
}
.mr-source-target {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 41bea0ec5c8..6eb659dae17 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -50,7 +50,17 @@
}
.project-home-dropdown {
- margin: 11px 3px 0;
+ margin: 13px 0px 0;
+ }
+
+ .notifications-btn {
+ .fa-bell {
+ margin-right: 6px;
+ }
+
+ .fa-angle-down {
+ margin-left: 6px;
+ }
}
.project-home-desc {
@@ -85,6 +95,7 @@
color: inherit;
}
}
+
.input-group {
display: inline-table;
position: relative;
@@ -233,23 +244,11 @@
}
}
- .fa-fw {
+ i {
margin-right: 8px;
}
}
-.fa-bell {
- margin-right: 6px;
-}
-
-.fa-angle-down {
- margin-left: 6px;
-}
-
-.project-home-panel .project-home-dropdown {
- margin: 13px 0px 0;
-}
-
.project-visibility-level-holder {
.radio {
margin-bottom: 10px;
@@ -544,5 +543,13 @@ pre.light-well {
}
.project-show-readme .readme-holder {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ padding: ($gl-padding + 7px);
border-top: 0;
+
+ .edit-project-readme {
+ z-index: 100;
+ position: relative;
+ }
}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 2b15ab83129..a9111a7388f 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -1,36 +1,34 @@
-.ci-body {
- .runner-state {
- padding: 6px 12px;
- margin-right: 10px;
- color: #FFF;
+.runner-state {
+ padding: 6px 12px;
+ margin-right: 10px;
+ color: #FFF;
- &.runner-state-shared {
- background: #32b186;
- }
- &.runner-state-specific {
- background: #3498db;
- }
+ &.runner-state-shared {
+ background: #32b186;
}
-
- .runner-status-online {
- color: green;
+ &.runner-state-specific {
+ background: #3498db;
}
+}
- .runner-status-offline {
- color: gray;
- }
+.runner-status-online {
+ color: green;
+}
- .runner-status-paused {
- color: red;
- }
+.runner-status-offline {
+ color: gray;
+}
+
+.runner-status-paused {
+ color: red;
+}
- .runner {
- .btn {
- padding: 1px 6px;
- }
+.runner {
+ .btn {
+ padding: 1px 6px;
+ }
- h4 {
- font-weight: normal;
- }
+ h4 {
+ font-weight: normal;
}
}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index a3d7aba054d..242783a7b7e 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -1,8 +1,3 @@
-.my-snippets li:first-child {
- h4 { margin-top: 0; }
- padding-top: 0;
-}
-
.snippet-form-holder .file-holder .file-title {
padding: 2px;
}
@@ -30,3 +25,58 @@
}
}
}
+
+.snippet-holder {
+ .snippet-details {
+ .page-title {
+ margin-top: -15px;
+ padding: 10px 0;
+ margin-bottom: 0;
+ color: #5c5d5e;
+ font-size: 16px;
+
+ .author {
+ color: #5c5d5e;
+ }
+
+ .snippet-id {
+ color: #5c5d5e;
+ }
+ }
+
+ .snippet-title {
+ margin: 0;
+ font-size: 23px;
+ color: #313236;
+ }
+
+ @media (max-width: $screen-md-max) {
+ .new-snippet-link {
+ display: none;
+ }
+ }
+
+ @media (max-width: $screen-sm-max) {
+ .creator,
+ .page-title .btn-close {
+ display: none;
+ }
+ }
+ }
+
+ .file-holder {
+ border-top: 0;
+ }
+}
+
+
+.snippet-box {
+ @include border-radius(2px);
+
+ display: inline-block;
+ padding: 10px $gl-padding;
+ font-weight: normal;
+ margin-right: 10px;
+ font-size: $gl-font-size;
+ border: 1px solid;
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 1b0cef481d6..d4ab6967ccd 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -5,7 +5,7 @@
tr {
> td, > th {
- line-height: 32px;
+ line-height: 28px;
}
&:hover {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 865deb7d46a..1b0609e279e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -124,7 +124,6 @@ class ApplicationController < ActionController::Base
project_path = "#{namespace}/#{id}"
@project = Project.find_with_namespace(project_path)
-
if @project and can?(current_user, :read_project, @project)
if @project.path_with_namespace != project_path
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return
diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb
index 110954a612d..0cafad27418 100644
--- a/app/controllers/ci/admin/runners_controller.rb
+++ b/app/controllers/ci/admin/runners_controller.rb
@@ -17,6 +17,7 @@ module Ci
@projects = @projects.where(gitlab_id: @gl_projects.select(:id))
end
@projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any?
+ @projects = @projects.joins(:gl_project)
@projects = @projects.page(params[:page]).per(30)
end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
index 9be470660e6..848f2b4e314 100644
--- a/app/controllers/ci/application_controller.rb
+++ b/app/controllers/ci/application_controller.rb
@@ -8,14 +8,6 @@ module Ci
private
- def authenticate_public_page!
- unless project.public
- authenticate_user!
-
- return access_denied! unless can?(current_user, :read_project, gl_project)
- end
- end
-
def authenticate_token!
unless project.valid_token?(params[:token])
return head(403)
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 816012762ce..7d72e0b951b 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -2,23 +2,24 @@ class Projects::BuildsController < Projects::ApplicationController
before_action :ci_project
before_action :build, except: [:index, :cancel_all]
- before_action :authorize_admin_project!, except: [:index, :show, :status]
+ before_action :authorize_manage_builds!, except: [:index, :show, :status]
layout "project"
def index
@scope = params[:scope]
@all_builds = project.ci_builds
+ @builds = @all_builds.order('created_at DESC')
@builds =
case @scope
when 'all'
- @all_builds
+ @builds
when 'finished'
- @all_builds.finished
+ @builds.finished
else
- @all_builds.running_or_pending
+ @builds.running_or_pending.reverse_order
end
- @builds = @builds.order('created_at DESC').page(params[:page]).per(30)
+ @builds = @builds.page(params[:page]).per(30)
end
def cancel_all
@@ -73,4 +74,10 @@ class Projects::BuildsController < Projects::ApplicationController
def build_path(build)
namespace_project_build_path(build.gl_project.namespace, build.gl_project, build)
end
+
+ def authorize_manage_builds!
+ unless can?(current_user, :manage_builds, project)
+ return page_404
+ end
+ end
end
diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb
index 406f313ae79..550a019e8e2 100644
--- a/app/controllers/projects/ci_services_controller.rb
+++ b/app/controllers/projects/ci_services_controller.rb
@@ -14,17 +14,17 @@ class Projects::CiServicesController < Projects::ApplicationController
end
def update
- if @service.update_attributes(service_params)
- redirect_to edit_namespace_project_ci_service_path(@project, @project.namespace, @service.to_param)
+ if service.update_attributes(service_params)
+ redirect_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param)
else
render 'edit'
end
end
def test
- last_build = @project.builds.last
+ last_build = @project.ci_builds.last
- if @service.execute(last_build)
+ if service.execute(last_build)
message = { notice: 'We successfully tested the service' }
else
message = { alert: 'We tried to test the service but error occurred' }
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 7886f3c6deb..878c3a66e7d 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -4,7 +4,8 @@
class Projects::CommitController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!
+ before_action :authorize_download_code!, except: [:cancel_builds]
+ before_action :authorize_manage_builds!, only: [:cancel_builds]
before_action :commit
def show
@@ -55,4 +56,12 @@ class Projects::CommitController < Projects::ApplicationController
def commit
@commit ||= @project.commit(params[:id])
end
+
+ private
+
+ def authorize_manage_builds!
+ unless can?(current_user, :manage_builds, project)
+ return page_404
+ end
+ end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index d1c15174aea..58fb946dbc2 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -12,7 +12,7 @@ class Projects::CommitsController < Projects::ApplicationController
@limit, @offset = (params[:limit] || 40), (params[:offset] || 0)
@commits = @repo.commits(@ref, @path, @limit, @offset)
- @note_counts = Note.where(commit_id: @commits.map(&:id)).
+ @note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
respond_to do |format|
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index deb07a21416..bfbcf2567f3 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -6,11 +6,10 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
- @runners = @ci_project.runners.order('id DESC')
- @specific_runners =
- Ci::Runner.specific.includes(:runner_projects).
- where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ).
- where.not(id: @runners).order("#{Ci::Runner.table_name}.id DESC").page(params[:page]).per(20)
+ @runners = @ci_project.runners.ordered
+ @specific_runners = current_user.ci_authorized_runners.
+ where.not(id: @ci_project.runners).
+ ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index b07a2a8db2f..2104c7a7a71 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -21,6 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController
filter: :by_project,
project: @project
})
+ @snippets = @snippets.page(params[:page]).per(PER_PAGE)
end
def new
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 82119022cf9..05c7d3de8bc 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -124,11 +124,7 @@ class ProjectsController < ApplicationController
::Projects::DestroyService.new(@project, current_user, {}).execute
flash[:alert] = "Project '#{@project.name}' was deleted."
- if request.referer.include?('/admin')
- redirect_to admin_namespaces_projects_path
- else
- redirect_to dashboard_projects_path
- end
+ redirect_back_or_default(default: dashboard_projects_path, options: {})
rescue Projects::DestroyService::DestroyError => ex
redirect_to edit_project_path(@project), alert: ex.message
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index dbd1e26fa79..ed88df5dd86 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -42,4 +42,13 @@ module CiStatusHelper
icon(icon_name)
end
+
+ def render_ci_status(ci_commit)
+ link_to ci_status_path(ci_commit),
+ class: "c#{ci_status_color(ci_commit)}",
+ title: "Build status: #{ci_commit.status}",
+ data: { toggle: 'tooltip', placement: 'left' } do
+ ci_status_icon(ci_commit)
+ end
+ end
end
diff --git a/app/helpers/clipboard_helper.rb b/app/helpers/clipboard_helper.rb
new file mode 100644
index 00000000000..3c1d7569fac
--- /dev/null
+++ b/app/helpers/clipboard_helper.rb
@@ -0,0 +1,8 @@
+module ClipboardHelper
+ def clipboard_button
+ content_tag :button,
+ icon('clipboard'),
+ class: 'btn btn-xs btn-clipboard js-clipboard-trigger',
+ type: :button
+ end
+end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 0e7d8065ac7..04e53fe7c61 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -110,22 +110,4 @@ module TabHelper
'active'
end
end
-
- # Use nav_tab for save controller/action but different params
- def nav_tab(key, value, &block)
- o = {}
- o[:class] = ""
-
- if value.nil?
- o[:class] << " active" if params[key].blank?
- else
- o[:class] << " active" if params[key] == value
- end
-
- if block_given?
- content_tag(:li, capture(&block), o)
- else
- content_tag(:li, nil, o)
- end
- end
end
diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb
index eb65c773570..4e806ca1a68 100644
--- a/app/models/ci/project.rb
+++ b/app/models/ci/project.rb
@@ -99,6 +99,7 @@ module Ci
def ordered_by_last_commit_date
last_commit_subquery = "(SELECT gl_project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY gl_project_id)"
joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id").
+ joins(:gl_project).
order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC")
end
end
diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb
index b66f1212f23..2d35aeac225 100644
--- a/app/models/ci/project_status.rb
+++ b/app/models/ci/project_status.rb
@@ -27,9 +27,5 @@ module Ci
def human_status
status
end
-
- def last_commit_for_ref(ref)
- commits.where(ref: ref).last
- end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 1b3669f1b7a..b719ad3c87e 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -36,6 +36,7 @@ module Ci
scope :active, ->() { where(active: true) }
scope :paused, ->() { where(active: false) }
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
+ scope :ordered, ->() { order(id: :desc) }
acts_as_taggable
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 8188ba3a28e..0b73ab6d2eb 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -20,7 +20,6 @@ class CommitStatus < ActiveRecord::Base
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) }
- scope :running_or_pending, -> { where(status: [:running, :pending]) }
state_machine :status, initial: :pending do
event :run do
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 21861a46a84..85f37e49e62 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -159,11 +159,11 @@ class MergeRequest < ActiveRecord::Base
def last_commit
merge_request_diff ? merge_request_diff.last_commit : compare_commits.last
- end
+ end
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
- end
+ end
def last_commit_short_sha
last_commit.short_id
@@ -257,7 +257,7 @@ class MergeRequest < ActiveRecord::Base
Note.where(
"(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
- "(project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
+ "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
mr_id: id,
commit_ids: commit_ids,
target_project_id: target_project_id,
@@ -470,4 +470,10 @@ class MergeRequest < ActiveRecord::Base
unlock_mr if locked?
end
end
+
+ def ci_commit
+ if last_commit
+ source_project.ci_commit(last_commit.id)
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 88cd88dcb5a..74b89aad499 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -243,11 +243,12 @@ class Project < ActiveRecord::Base
# Use of unscoped ensures we're not secretly adding any ORDER BYs, which
# have a negative impact on performance (and aren't needed for this
# query).
- unscoped.
+ projects = unscoped.
joins(:namespace).
- iwhere('namespaces.path' => namespace_path).
- iwhere('projects.path' => project_path).
- take
+ iwhere('namespaces.path' => namespace_path)
+
+ projects.where('projects.path' => project_path).take ||
+ projects.iwhere('projects.path' => project_path).take
end
def visibility_levels
@@ -567,7 +568,7 @@ class Project < ActiveRecord::Base
end
def empty_repo?
- !repository.exists? || repository.empty?
+ !repository.exists? || !repository.has_visible_content?
end
def repo
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 0808896fd87..a3ba5f4c18a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -44,6 +44,19 @@ class Repository
raw_repository.empty?
end
+ #
+ # Git repository can contains some hidden refs like:
+ # /refs/notes/*
+ # /refs/git-as-svn/*
+ # /refs/pulls/*
+ # This refs by default not visible in project page and not cloned to client side.
+ #
+ # This method return true if repository contains some content visible in project page.
+ #
+ def has_visible_content?
+ !raw_repository.branches.empty?
+ end
+
def commit(id = 'HEAD')
return nil unless raw_repository
commit = Gitlab::Git::Commit.find(raw_repository, id)
@@ -54,13 +67,16 @@ class Repository
end
def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false)
- commits = Gitlab::Git::Commit.where(
+ options = {
repo: raw_repository,
ref: ref,
path: path,
limit: limit,
offset: offset,
- )
+ follow: path.present?
+ }
+
+ commits = Gitlab::Git::Commit.where(options)
commits = Commit.decorate(commits, @project) if commits.present?
commits
end
@@ -480,7 +496,7 @@ class Repository
def search_files(query, ref)
offset = 2
- args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref})
+ args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 7e4321d5376..c72beacbf0f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -401,15 +401,17 @@ class User < ActiveRecord::Base
end
end
+ def authorized_projects_id
+ @authorized_projects_id ||= begin
+ project_ids = personal_projects.pluck(:id)
+ project_ids.push(*groups_projects.pluck(:id))
+ project_ids.push(*projects.pluck(:id).uniq)
+ end
+ end
# Projects user has access to
def authorized_projects
- @authorized_projects ||= begin
- project_ids = personal_projects.pluck(:id)
- project_ids.push(*groups_projects.pluck(:id))
- project_ids.push(*projects.pluck(:id).uniq)
- Project.where(id: project_ids)
- end
+ @authorized_projects ||= Project.where(id: authorized_projects_id)
end
def owned_projects
@@ -768,11 +770,14 @@ class User < ActiveRecord::Base
end
def ci_authorized_projects
- @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects)
+ @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects_id)
end
def ci_authorized_runners
- Ci::Runner.specific.includes(:runner_projects).
- where(ci_runner_projects: { project_id: ci_authorized_projects } )
+ @ci_authorized_runners ||= begin
+ runner_ids = Ci::RunnerProject.joins(:project).
+ where(ci_projects: { gitlab_id: authorized_projects_id }).select(:runner_id)
+ Ci::Runner.specific.where(id: runner_ids)
+ end
end
end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index b95835ba093..b8d24193035 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,17 +1,15 @@
module Ci
class ImageForBuildService
def execute(project, params)
- image_name =
- if params[:sha]
- commit = project.commits.find_by(sha: params[:sha])
- image_for_commit(commit)
- elsif params[:ref]
- commit = project.last_commit_for_ref(params[:ref])
- image_for_commit(commit)
- else
- 'build-unknown.svg'
+ sha = params[:sha]
+ sha ||=
+ if params[:ref]
+ project.gl_project.commit(params[:ref]).try(:sha)
end
+ commit = project.commits.ordered.find_by(sha: sha)
+ image_name = image_for_commit(commit)
+
image_path = Rails.root.join('public/ci', image_name)
OpenStruct.new(
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 121f6899011..d68bc79ecc0 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -5,20 +5,19 @@ module MergeRequests
@oldrev, @newrev = oldrev, newrev
@branch_name = Gitlab::Git.ref_name(ref)
- @fork_merge_requests = @project.fork_merge_requests.opened
- @commits = []
- # Leave a system note if a branch were deleted/added
- if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
+ find_new_commits
+ reload_merge_requests
+
+ # Leave a system note if a branch was deleted/added
+ if branch_added? || branch_removed?
comment_mr_branch_presence_changed
- comment_mr_with_commits if @commits.present?
+ comment_mr_with_commits
else
- @commits = @project.repository.commits_between(oldrev, newrev)
comment_mr_with_commits
close_merge_requests
end
- reload_merge_requests
execute_mr_web_hooks
true
@@ -54,7 +53,7 @@ module MergeRequests
# Note: we should update merge requests from forks too
def reload_merge_requests
merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a
- merge_requests += @fork_merge_requests.by_branch(@branch_name).to_a
+ merge_requests += fork_merge_requests.by_branch(@branch_name).to_a
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request|
@@ -77,29 +76,37 @@ module MergeRequests
end
end
- # Add comment about branches being deleted or added to merge requests
- def comment_mr_branch_presence_changed
- presence = Gitlab::Git.blank_ref?(@oldrev) ? :add : :delete
+ def find_new_commits
+ if branch_added?
+ @commits = []
- merge_requests_for_source_branch.each do |merge_request|
- last_commit = merge_request.last_commit
+ merge_request = merge_requests_for_source_branch.first
+ return unless merge_request
- # Only look at changed commits in restore branch case
- unless Gitlab::Git.blank_ref?(@newrev)
- begin
- # Since any number of commits could have been made to the restored branch,
- # find the common root to see what has been added.
- common_ref = @project.repository.merge_base(last_commit.id, @newrev)
- # If the a commit no longer exists in this repo, gitlab_git throws
- # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52
- @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref
- rescue
- end
+ last_commit = merge_request.last_commit
- # Prevent system notes from seeing a blank SHA
- @oldrev = nil
+ begin
+ # Since any number of commits could have been made to the restored branch,
+ # find the common root to see what has been added.
+ common_ref = @project.repository.merge_base(last_commit.id, @newrev)
+ # If the a commit no longer exists in this repo, gitlab_git throws
+ # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52
+ @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref
+ rescue
end
+ elsif branch_removed?
+ # No commits for a deleted branch.
+ @commits = []
+ else
+ @commits = @project.repository.commits_between(@oldrev, @newrev)
+ end
+ end
+
+ # Add comment about branches being deleted or added to merge requests
+ def comment_mr_branch_presence_changed
+ presence = branch_added? ? :add : :delete
+ merge_requests_for_source_branch.each do |merge_request|
SystemNoteService.change_branch_presence(
merge_request, merge_request.project, @current_user,
:source, @branch_name, presence)
@@ -108,6 +115,8 @@ module MergeRequests
# Add comment about pushing new commits to merge requests
def comment_mr_with_commits
+ return unless @commits.present?
+
merge_requests_for_source_branch.each do |merge_request|
mr_commit_ids = Set.new(merge_request.commits.map(&:id))
@@ -135,9 +144,21 @@ module MergeRequests
def merge_requests_for_source_branch
@source_merge_requests ||= begin
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
- merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a
+ merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a
filter_merge_requests(merge_requests)
end
end
+
+ def fork_merge_requests
+ @fork_merge_requests ||= @project.fork_merge_requests.opened
+ end
+
+ def branch_added?
+ Gitlab::Git.blank_ref?(@oldrev)
+ end
+
+ def branch_removed?
+ Gitlab::Git.blank_ref?(@newrev)
+ end
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 37f454cfc3f..708c2f00486 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -327,7 +327,7 @@ class SystemNoteService
commit_ids = if count == 1
existing_commits.first.short_id
else
- if oldrev
+ if oldrev && !Gitlab::Git.blank_ref?(oldrev)
"#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
else
"#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml
index f049b4f4c4e..6b4e3b2cb38 100644
--- a/app/views/ci/admin/runner_projects/index.html.haml
+++ b/app/views/ci/admin/runner_projects/index.html.haml
@@ -1,5 +1,5 @@
%p.lead
- To register new runner visit #{link_to 'this page ', ci_runners_path}
+ To register a new runner visit #{link_to 'this page ', ci_runners_path}
.row
.col-md-8
diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml
index bb213fbffc4..bacaccfbffa 100644
--- a/app/views/ci/admin/runners/index.html.haml
+++ b/app/views/ci/admin/runners/index.html.haml
@@ -1,5 +1,5 @@
%p.lead
- %span To register new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication.
+ %span To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication.
%code #{GitlabCi::REGISTRATION_TOKEN}
.bs-callout
@@ -21,7 +21,7 @@
\- run builds from assigned projects
%li
%span.label.label-danger paused
- \- runner will not receive any new build
+ \- runner will not receive any new builds
.append-bottom-20.clearfix
.pull-left
diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml
index 92787b2e6ac..1498db46a80 100644
--- a/app/views/ci/admin/runners/show.html.haml
+++ b/app/views/ci/admin/runners/show.html.haml
@@ -13,13 +13,13 @@
- if @runner.shared?
.bs-callout.bs-callout-success
- %h4 This runner will process build from ALL UNASSIGNED projects
+ %h4 This runner will process builds from ALL UNASSIGNED projects
%p
If you want runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
- else
.bs-callout.bs-callout-info
- %h4 This runner will process build only from ASSIGNED projects
+ %h4 This runner will process builds only from ASSIGNED projects
%p You can't make this a shared runner.
%hr
= form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f|
@@ -53,13 +53,14 @@
%th
- @runner.runner_projects.each do |runner_project|
- project = runner_project.project
- %tr.alert-info
- %td
- %strong
- = project.name
- %td
- .pull-right
- = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
+ - if project.gl_project
+ %tr.alert-info
+ %td
+ %strong
+ = project.name
+ %td
+ .pull-right
+ = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
%table.table
%thead
@@ -103,21 +104,26 @@
%th Finished at
- @builds.each do |build|
+ - gl_project = build.gl_project
%tr.build
%td.id
- - gl_project = build.project.gl_project
- = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
+ - if gl_project
+ = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
+ = build.id
+ - else
= build.id
%td.status
= ci_status_with_icon(build.status)
%td.status
- = build.project.name
+ - if gl_project
+ = gl_project.name_with_namespace
%td.build-link
- = link_to ci_status_path(build.commit) do
- %strong #{build.commit.short_sha}
+ - if gl_project
+ = link_to ci_status_path(build.commit) do
+ %strong #{build.commit.short_sha}
%td.timestamp
- if build.finished_at
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index f45cd05aec0..77f78caa8d8 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -17,7 +17,7 @@
%td #{stage.capitalize} Job - #{build[:name]}
%td
%pre
- = simple_format build[:script]
+ = simple_format build[:commands]
%br
%b Tag list:
@@ -28,6 +28,11 @@
%br
%b Refs except:
= build[:except] && build[:except].join(", ")
+ %br
+ %b When:
+ = build[:when]
+ - if build[:allow_failure]
+ %b Allowed to fail
-else
%p
diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml
index 308b217ea78..b8d9a1d7089 100644
--- a/app/views/ci/user_sessions/new.html.haml
+++ b/app/views/ci/user_sessions/new.html.haml
@@ -1,8 +1,7 @@
.login-block
%h2 Login using GitLab account
%p.light
- Make sure you have account on GitLab server
+ Make sure you have an account on the GitLab server
= link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink
%hr
= link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' )
-
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index d3908062f43..07b6d57932e 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -6,33 +6,29 @@
.gray-content-block
.pull-right
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ = icon('plus')
+ New Snippet
- .oneline
- Share code pastes with others out of git repository
-
-%ul.nav.nav-tabs.prepend-top-20
- = nav_tab :scope, nil do
- = link_to dashboard_snippets_path do
+ .btn-group.btn-group-next.snippet-scope-menu
+ = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
All
%span.badge
= current_user.snippets.count
- = nav_tab :scope, 'are_private' do
- = link_to dashboard_snippets_path(scope: 'are_private') do
+
+ = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
Private
%span.badge
= current_user.snippets.are_private.count
- = nav_tab :scope, 'are_internal' do
- = link_to dashboard_snippets_path(scope: 'are_internal') do
+
+ = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
Internal
%span.badge
= current_user.snippets.are_internal.count
- = nav_tab :scope, 'are_public' do
- = link_to dashboard_snippets_path(scope: 'are_public') do
+
+ = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
Public
%span.badge
= current_user.snippets.are_public.count
-.my-snippets
- = render 'snippets/snippets'
+= render 'snippets/snippets'
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 7e4fa7d4873..0f100c39ffb 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -10,7 +10,8 @@
- if current_user
.pull-right
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ = icon('plus')
+ New Snippet
.oneline
Public snippets created by you and other users are listed here
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 0a1cecfdcdf..b5ef0aca540 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -1,10 +1,8 @@
- if readme = @repository.readme
- %article.file-holder.readme-holder
- .file-title
- = blob_icon readme.mode, readme.name
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
- %strong
- = readme.name
+ %article.readme-holder
+ .pull-right
+ - if can?(current_user, :push_code, @project)
+ = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
.file-content.wiki
= cache(readme_cache_key) do
= render_readme(readme)
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index c45bfb27b8f..e3d8d734913 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -155,7 +155,7 @@
- if @builds.present?
.build-widget
- %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}:
+ %h4.title #{pluralize(@builds.count(:id), "other build")} for #{@build.short_sha}:
%table.table.builds
- @builds.each_with_index do |build, i|
%tr.build
@@ -175,4 +175,4 @@
:javascript
- new CiBuild("#{namespace_project_build_path(@project.namespace, @project, @build)}", "#{@build.status}")
+ new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}")
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
index 0c298844912..3e83ec3912f 100644
--- a/app/views/projects/buttons/_notifications.html.haml
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -5,7 +5,7 @@
= hidden_field_tag :notification_id, @membership.id
= hidden_field_tag :notification_level
%span.dropdown
- %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"}
= icon('bell')
= notification_label(@membership)
= icon('angle-down')
@@ -14,7 +14,7 @@
= notification_list_item(level, @membership)
- when GroupMember
- .btn.btn-new.disabled.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
+ .btn.disabled.notifications-btn.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
= icon('bell')
= notification_label(@membership)
= icon('angle-down')
diff --git a/app/views/projects/ci_settings/_no_runners.html.haml b/app/views/projects/ci_settings/_no_runners.html.haml
index 33038c52978..1374e6680f9 100644
--- a/app/views/projects/ci_settings/_no_runners.html.haml
+++ b/app/views/projects/ci_settings/_no_runners.html.haml
@@ -5,4 +5,4 @@
You can add Specific runner for this project on Runners page
- if current_user.admin
- or add Shared runner for whole application in admin are.
+ or add Shared runner for whole application in admin area.
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 9e0b536bb4b..80f25ed1296 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -18,10 +18,10 @@
.pull-right
- if ci_commit
- = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do
- = ci_status_icon(ci_commit)
+ = render_ci_status(ci_commit)
&nbsp;
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+ = clipboard_button
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id", data: {clipboard_text: commit.id}
.notes_count
- if note_count > 0
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index e06454fd148..c3858e78cad 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -2,53 +2,56 @@
- if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh'
= render 'shared/no_password'
-
+
= render "home_panel"
.gray-content-block.center
%h3.page-title
The repository for this project is empty
- %p
- If you already have files you can push them using command line instructions below.
- %br
- Otherwise you can start with
- = link_to "adding README", new_readme_path, class: 'underlined-link'
- file to this project.
+ - if can?(current_user, :download_code, @project)
+ %p
+ If you already have files you can push them using command line instructions below.
+ %br
+ - if can?(current_user, :push_code, @project)
+ Otherwise you can start with
+ = link_to "adding README", new_readme_path, class: 'underlined-link'
+ file to this project.
-.prepend-top-20
-.empty_wrapper
- %h3.page-title-empty
- Command line instructions
- %div.git-empty
- %fieldset
- %h5 Git global setup
- %pre.light-well
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+- if can?(current_user, :download_code, @project)
+ .prepend-top-20
+ .empty_wrapper
+ %h3.page-title-empty
+ Command line instructions
+ %div.git-empty
+ %fieldset
+ %h5 Git global setup
+ %pre.light-well
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.light-well
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- git push -u origin master
+ %fieldset
+ %h5 Create a new repository
+ %pre.light-well
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ git push -u origin master
- %fieldset
- %h5 Existing folder or Git repository
- %pre.light-well
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git add .
- git commit
- git push -u origin master
+ %fieldset
+ %h5 Existing folder or Git repository
+ %pre.light-well
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git add .
+ git commit
+ git push -u origin master
- - if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index d4a98eca473..c5fd863ae99 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -17,8 +17,10 @@
- @participants.each do |participant|
= link_to_member(@project, participant, name: false, size: 24)
.col-md-3
- %span.slead.has_tooltip{title: 'Cross-project reference'}
- = cross_project_reference(@project, @issue)
+ .input-group.cross-project-reference
+ %span.slead.has_tooltip{title: 'Cross-project reference'}
+ = cross_project_reference(@project, @issue)
+ = clipboard_button
.row
%section.col-md-9
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index a3399c57aa2..ca5b1a8386d 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -5,8 +5,9 @@
.nothing-here-block No issues to show
- if @issues.present?
- .pull-right
- %span.issue_counter #{@issues.total_count}
- issues for this filter
+ .issuable-filter-count
+ %span.pull-right
+ = @issues.total_count
+ issues for this filter
= paginate @issues, theme: "gitlab"
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 25e4e8ba80d..300a3715292 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,3 +1,4 @@
+- ci_commit = merge_request.ci_commit
%li{ class: mr_css_classes(merge_request) }
.merge-request-title
%span.merge-request-title-text
@@ -6,6 +7,8 @@
- merge_request.labels.each do |label|
= link_to_label(label, project: merge_request.project)
.pull-right.light
+ - if ci_commit
+ = render_ci_status(ci_commit)
- if merge_request.merged?
%span
%i.fa.fa-check
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index d86707b3d97..0af970e4b92 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -5,8 +5,10 @@
.nothing-here-block No merge requests to show
- if @merge_requests.present?
- .pull-right
- %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter
+ .issuable-filter-count
+ %span.pull-right
+ = @merge_requests.total_count
+ merge requests for this filter
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index f18cf96c17d..98f0357ce4e 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -3,11 +3,12 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3 Check out, review and merge locally
+ %h3 Check out, review, and merge locally
.modal-body
%p
- %strong Step 1.
+ %strong Step 1.
Fetch and check out the branch for this merge request
+ = clipboard_button
%pre.dark
- if @merge_request.for_fork?
:preserve
@@ -24,6 +25,7 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
+ = clipboard_button
%pre.dark
- if @merge_request.for_fork?
:preserve
@@ -36,6 +38,7 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
+ = clipboard_button
%pre.dark
:preserve
git push origin #{h @merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 10efb811939..a3551516bfe 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,4 +1,4 @@
-- ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha)
+- ci_commit = @merge_request.ci_commit
- if ci_commit
- status = ci_commit.status
.mr-widget-heading
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
new file mode 100644
index 00000000000..4a515469422
--- /dev/null
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -0,0 +1,11 @@
+= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do
+ = icon('plus')
+ New Snippet
+- if can?(current_user, :admin_project_snippet, @snippet)
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do
+ = icon('trash-o')
+ Delete
+- if can?(current_user, :update_project_snippet, @snippet)
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
+ = icon('pencil-square-o')
+ Edit
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 3fed2c9949d..4af963e14da 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,17 +1,13 @@
- page_title "Snippets"
= render "header_title"
-%h3.page-title
- Snippets
- - if can? current_user, :create_project_snippet, @project
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Snippet" do
- Add new snippet
+.gray-content-block.top-block
+ .pull-right
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
+ = icon('plus')
+ New Snippet
-%p.light
- Share code pastes with others out of git repository
+ .oneline
+ Share code pastes with others out of git repository
-%ul.bordered-list
- = render partial: "shared/snippets/snippet", collection: @snippets
- - if @snippets.empty?
- %li
- .nothing-here-block Nothing here.
+= render 'snippets/snippets'
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index be7d4d486fa..5d706942f2d 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,40 +1,18 @@
- page_title @snippet.title, "Snippets"
= render "header_title"
-%h3.page-title
- = @snippet.title
+.snippet-holder
+ = render 'shared/snippets/header'
- .pull-right
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ %article.file-holder
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ %strong
+ = @snippet.file_name
+ .file-actions.hidden-xs
+ .btn-group.tree-btn-group
+ = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
-%hr
+ = render 'shared/snippets/blob'
-.append-bottom-20
- .pull-right
- = "##{@snippet.id}"
- %span.light
- by
- = link_to user_path(@snippet.author) do
- = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16"
- = @snippet.author_name
-
- .back-link
- = link_to namespace_project_snippets_path(@project.namespace, @project) do
- &larr; project snippets
-
-.file-holder
- .file-title
- %i.fa.fa-file
- %strong
- = @snippet.file_name
- .file-actions
- .btn-group
- - if can?(current_user, :update_project_snippet, @snippet)
- = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet'
- = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
- - if can?(current_user, :admin_project_snippet, @snippet)
- = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet'
- = render 'shared/snippets/blob'
-
-%div#notes= render "projects/notes/notes_with_form"
+ %div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index aee839b44e7..c36995b94d7 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -21,9 +21,7 @@
.project-controls
- if ci && !project.empty_repo? && project.commit
- if ci_commit = project.ci_commit(project.commit.sha)
- = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}",
- title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do
- = ci_status_icon(ci_commit)
+ = render_ci_status(ci_commit)
&nbsp;
- if stars
%span
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
new file mode 100644
index 00000000000..0a4a790ec5e
--- /dev/null
+++ b/app/views/shared/snippets/_header.html.haml
@@ -0,0 +1,24 @@
+.snippet-details
+ .page-title
+ .snippet-box{class: visibility_level_color(@snippet.visibility_level)}
+ = visibility_level_icon(@snippet.visibility_level)
+ = visibility_level_label(@snippet.visibility_level)
+ %span.snippet-id Snippet ##{@snippet.id}
+ %span.creator
+ &middot; created by #{link_to_member(@project, @snippet.author, size: 24)}
+ &middot;
+ = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
+ - if @snippet.updated_at != @snippet.created_at
+ %span
+ &middot;
+ = icon('edit', title: 'edited')
+ = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
+
+ .pull-right
+ - if @snippet.project_id?
+ = render "projects/snippets/actions"
+ - else
+ = render "snippets/actions"
+ .gray-content-block.middle-block
+ %h2.snippet-title
+ = gfm escape_once(@snippet.title)
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 69a713ad9aa..c6294caddc7 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -18,4 +18,3 @@
= image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: ''
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
-
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
new file mode 100644
index 00000000000..751fafa8942
--- /dev/null
+++ b/app/views/snippets/_actions.html.haml
@@ -0,0 +1,11 @@
+= link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do
+ = icon('plus')
+ New Snippet
+- if can?(current_user, :admin_personal_snippet, @snippet)
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do
+ = icon('trash-o')
+ Delete
+- if can?(current_user, :update_personal_snippet, @snippet)
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ = icon('pencil-square-o')
+ Edit
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 97374e073dc..69d8899d4c1 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,41 +1,14 @@
- page_title @snippet.title, "Snippets"
-%h4.page-title
- = @snippet.title
- - if @snippet.private?
- %span.label.label-success
- %i.fa.fa-lock
- private
-
- .pull-right
- = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do
- Add new snippet
-
-.append-bottom-10.prepend-top-10
- .pull-right
- %span.light
- created by
- = link_to user_snippets_path(@snippet.author) do
- = @snippet.author_name
-
- .back-link
- - if @snippet.author == current_user
- = link_to dashboard_snippets_path do
- &larr; your snippets
- - else
- = link_to explore_snippets_path do
- &larr; explore snippets
-
-.file-holder
- .file-title
- %i.fa.fa-file
- %strong
- = @snippet.file_name
- .file-actions
- .btn-group
- - if can?(current_user, :update_personal_snippet, @snippet)
- = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet'
- = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
- - if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet'
- = render 'shared/snippets/blob'
+.snippet-holder
+ = render 'shared/snippets/header'
+
+ %article.file-holder
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ %strong
+ = @snippet.file_name
+ .file-actions.hidden-xs
+ .btn-group.tree-btn-group
+ = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
+ = render 'shared/snippets/blob'
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 8b85981497a..d3aef44705b 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -318,10 +318,12 @@ production: &base
# ==========================
# GitLab Satellites
+ #
+ # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at
+ # least. This setting is fed to 'rm -rf' in
+ # db/migrate/20151023144219_remove_satellites.rb
satellites:
- # Relative paths are relative to Rails.root (default: tmp/repo_satellites/)
path: /home/git/gitlab-satellites/
- timeout: 30
## Backup settings
backup:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index d5493ca038d..65e9b0dcb50 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -242,9 +242,11 @@ Settings.git['max_size'] ||= 20971520 # 20.megabytes
Settings.git['bin_path'] ||= '/usr/bin/git'
Settings.git['timeout'] ||= 10
+# Important: keep the satellites.path setting until GitLab 9.0 at
+# least. This setting is fed to 'rm -rf' in
+# db/migrate/20151023144219_remove_satellites.rb
Settings['satellites'] ||= Settingslogic.new({})
Settings.satellites['path'] = File.expand_path(Settings.satellites['path'] || "tmp/repo_satellites/", Rails.root)
-Settings.satellites['timeout'] ||= 30
#
# Extra customization
diff --git a/db/migrate/20151023112551_fail_build_with_empty_name.rb b/db/migrate/20151023112551_fail_build_with_empty_name.rb
new file mode 100644
index 00000000000..f069bc60ac7
--- /dev/null
+++ b/db/migrate/20151023112551_fail_build_with_empty_name.rb
@@ -0,0 +1,5 @@
+class FailBuildWithEmptyName < ActiveRecord::Migration
+ def change
+ execute("UPDATE ci_builds SET status='failed' WHERE (name IS NULL OR name='') AND status='pending'")
+ end
+end
diff --git a/db/migrate/20151023144219_remove_satellites.rb b/db/migrate/20151023144219_remove_satellites.rb
new file mode 100644
index 00000000000..e73f300028a
--- /dev/null
+++ b/db/migrate/20151023144219_remove_satellites.rb
@@ -0,0 +1,17 @@
+require 'fileutils'
+
+class RemoveSatellites < ActiveRecord::Migration
+ def up
+ satellites = Gitlab.config['satellites']
+ return if satellites.nil?
+
+ satellites_path = satellites['path']
+ return if satellites_path.nil?
+
+ FileUtils.rm_rf(satellites_path)
+ end
+
+ def down
+ # Do nothing
+ end
+end
diff --git a/db/migrate/20151026182941_add_project_path_index.rb b/db/migrate/20151026182941_add_project_path_index.rb
new file mode 100644
index 00000000000..a62fe199d70
--- /dev/null
+++ b/db/migrate/20151026182941_add_project_path_index.rb
@@ -0,0 +1,9 @@
+class AddProjectPathIndex < ActiveRecord::Migration
+ def up
+ add_index :projects, :path
+ end
+
+ def down
+ remove_index :projects, :path
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0fec00ebf8f..4bde9f0b748 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20151020173906) do
+ActiveRecord::Schema.define(version: 20151026182941) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -624,6 +624,7 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
+ add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
create_table "protected_branches", force: true do |t|
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 191e3a8144d..ef8a7ec1e86 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -90,7 +90,7 @@ you need to set MYSQL_ALLOW_EMPTY_PASSWORD.
- mysql
variables:
- MYSQL_ALLOW_EMPTY_PASSWORD: yes
+ MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
```
For other possible configuration variables check the
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2e9ac7393e3..36d6ec79fde 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -346,11 +346,6 @@ The `secrets.yml` file stores encryption keys for sessions and secure variables.
Backup `secrets.yml` someplace safe, but don't store it in the same place as your database backups.
Otherwise your secrets are exposed if one of your backups is compromised.
-### Install schedules
-
- # Setup schedules
- sudo -u gitlab_ci -H bundle exec whenever -w RAILS_ENV=production
-
### Install Init Script
Download the init script (will be `/etc/init.d/gitlab`):
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 06f582dcee8..606532a6fbe 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -29,7 +29,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
Also you can choose what should be backed up by adding environment variable SKIP. Available options: db,
-uploads (attachments), repositories. Use a comma to specify several options at the same time.
+uploads (attachments), repositories, builds(CI build output logs). Use a comma to specify several options at the same time.
```
sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index f608674faf6..9a24a1e252a 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -26,7 +26,7 @@ After getting used to these three steps the branching model becomes the challeng
Since many organizations new to git have no conventions how to work with it, it can quickly become a mess.
The biggest problem they run into is that many long running branches that each contain part of the changes are around.
People have a hard time figuring out which branch they should develop on or deploy to production.
-Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html)
+Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html).
We think there is still room for improvement and will detail a set of practices we call GitLab flow.
## Git flow and its problems
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 83055188bac..f423c3ba542 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -10,6 +10,12 @@ Feature: Project Merge Requests
Then I should see "Bug NS-04" in merge requests
And I should not see "Feature NS-03" in merge requests
+ Scenario: I should see CI status for merge requests
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ Given "Bug NS-05" has CI status
+ When I visit project "Shop" merge requests page
+ Then I should see merge request "Bug NS-05" with CI status
+
Scenario: I should see rejected merge requests
Given I click link "Closed"
Then I should see "Feature NS-03" in merge requests
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
index 77e42a1a38b..270557cbde7 100644
--- a/features/project/snippets.feature
+++ b/features/project/snippets.feature
@@ -30,5 +30,5 @@ Feature: Project Snippets
Scenario: I destroy "Snippet one"
Given I visit snippet page "Snippet one"
- And I click link "Remove Snippet"
+ And I click link "Delete"
Then I should not see "Snippet one" in snippets
diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature
index 4f617b6bed8..e15d7c79342 100644
--- a/features/snippets/snippets.feature
+++ b/features/snippets/snippets.feature
@@ -24,7 +24,7 @@ Feature: Snippets
Scenario: I destroy "Personal snippet one"
Given I visit snippet page "Personal snippet one"
- And I click link "Destroy"
+ And I click link "Delete"
Then I should not see "Personal snippet one" in snippets
Scenario: I create new internal snippet
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 875bf6c4676..92ec14d0d76 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -338,6 +338,19 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content('diff --git')
end
+ step '"Bug NS-05" has CI status' do
+ project = merge_request.source_project
+ project.enable_ci
+ ci_commit = create :ci_commit, gl_project: project, sha: merge_request.last_commit.id
+ create :ci_build, commit: ci_commit
+ end
+
+ step 'I should see merge request "Bug NS-05" with CI status' do
+ page.within ".mr-list" do
+ expect(page).to have_link "Build status: pending"
+ end
+ end
+
def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index db8ad08bb9e..a3aef9bf8c3 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "New Snippet"' do
- click_link "Add new snippet"
+ click_link "New Snippet"
end
step 'I click link "Snippet one"' do
@@ -42,13 +42,13 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "Edit"' do
- page.within ".file-title" do
+ page.within ".page-title" do
click_link "Edit"
end
end
- step 'I click link "Remove Snippet"' do
- click_link "remove"
+ step 'I click link "Delete"' do
+ click_link "Delete"
end
step 'I submit new snippet "Snippet three"' do
diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb
index 6ff48e0c6b8..80d1ddeef05 100644
--- a/features/steps/snippets/snippets.rb
+++ b/features/steps/snippets/snippets.rb
@@ -13,13 +13,13 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps
end
step 'I click link "Edit"' do
- page.within ".file-title" do
+ page.within ".page-title" do
click_link "Edit"
end
end
- step 'I click link "Destroy"' do
- click_link "remove"
+ step 'I click link "Delete"' do
+ click_link "Delete"
end
step 'I submit new snippet "Personal snippet three"' do
diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb
index dea3256229f..997c605bce2 100644
--- a/features/steps/snippets/user.rb
+++ b/features/steps/snippets/user.rb
@@ -32,19 +32,19 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps
end
step 'I click "Internal" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Internal"
end
end
step 'I click "Private" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Private"
end
end
step 'I click "Public" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Public"
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index afc0402f9e1..40671e2517c 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -25,7 +25,7 @@ module API
format :json
content_type :txt, "text/plain"
- helpers APIHelpers
+ helpers Helpers
mount Groups
mount GroupMembers
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 549b1f9e9a7..652bdf9b278 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,5 +1,5 @@
module API
- module APIHelpers
+ module Helpers
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER ="HTTP_SUDO"
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 218d8c3adcc..0a4cbf69b63 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -26,7 +26,7 @@ module Ci
format :json
helpers Helpers
- helpers ::API::APIHelpers
+ helpers ::API::Helpers
mount Builds
mount Commits
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 0da73e387e1..efcd2faffc7 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -139,66 +139,74 @@ module Ci
end
@jobs.each do |name, job|
- validate_job!("#{name} job", job)
+ validate_job!(name, job)
end
true
end
def validate_job!(name, job)
+ if name.blank? || !validate_string(name)
+ raise ValidationError, "job name should be non-empty string"
+ end
+
job.keys.each do |key|
unless ALLOWED_JOB_KEYS.include? key
- raise ValidationError, "#{name}: unknown parameter #{key}"
+ raise ValidationError, "#{name} job: unknown parameter #{key}"
end
end
- if !job[:script].is_a?(String) && !validate_array_of_strings(job[:script])
- raise ValidationError, "#{name}: script should be a string or an array of a strings"
+ if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
+ raise ValidationError, "#{name} job: script should be a string or an array of a strings"
end
if job[:stage]
unless job[:stage].is_a?(String) && job[:stage].in?(stages)
- raise ValidationError, "#{name}: stage parameter should be #{stages.join(", ")}"
+ raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}"
end
end
- if job[:image] && !job[:image].is_a?(String)
- raise ValidationError, "#{name}: image should be a string"
+ if job[:image] && !validate_string(job[:image])
+ raise ValidationError, "#{name} job: image should be a string"
end
if job[:services] && !validate_array_of_strings(job[:services])
- raise ValidationError, "#{name}: services should be an array of strings"
+ raise ValidationError, "#{name} job: services should be an array of strings"
end
if job[:tags] && !validate_array_of_strings(job[:tags])
- raise ValidationError, "#{name}: tags parameter should be an array of strings"
+ raise ValidationError, "#{name} job: tags parameter should be an array of strings"
end
if job[:only] && !validate_array_of_strings(job[:only])
- raise ValidationError, "#{name}: only parameter should be an array of strings"
+ raise ValidationError, "#{name} job: only parameter should be an array of strings"
end
if job[:except] && !validate_array_of_strings(job[:except])
- raise ValidationError, "#{name}: except parameter should be an array of strings"
+ raise ValidationError, "#{name} job: except parameter should be an array of strings"
end
if job[:allow_failure] && !job[:allow_failure].in?([true, false])
- raise ValidationError, "#{name}: allow_failure parameter should be an boolean"
+ raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
end
if job[:when] && !job[:when].in?(%w(on_success on_failure always))
- raise ValidationError, "#{name}: when parameter should be on_success, on_failure or always"
+ raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always"
end
end
private
def validate_array_of_strings(values)
- values.is_a?(Array) && values.all? {|tag| tag.is_a?(String)}
+ values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
def validate_variables(variables)
- variables.is_a?(Hash) && variables.all? {|key, value| key.is_a?(Symbol) && value.is_a?(String)}
+ variables.is_a?(Hash) && variables.all? { |key, value| validate_string(key) && validate_string(value) }
+ end
+
+ def validate_string(value)
+ value.is_a?(String) || value.is_a?(Symbol)
end
end
end
diff --git a/lib/ci/migrate/builds.rb b/lib/ci/migrate/builds.rb
deleted file mode 100644
index c4f62e55295..00000000000
--- a/lib/ci/migrate/builds.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Ci
- module Migrate
- class Builds
- attr_reader :app_builds_dir, :backup_builds_tarball, :backup_dir
-
- def initialize
- @app_builds_dir = Settings.gitlab_ci.builds_path
- @backup_dir = Gitlab.config.backup.path
- @backup_builds_tarball = File.join(backup_dir, 'builds/builds.tar.gz')
- end
-
- def restore
- backup_existing_builds_dir
-
- FileUtils.mkdir_p(app_builds_dir, mode: 0700)
- unless system('tar', '-C', app_builds_dir, '-zxf', backup_builds_tarball)
- abort 'Restore failed'.red
- end
- end
-
- def backup_existing_builds_dir
- timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}")
- if File.exists?(app_builds_dir)
- FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path))
- end
- end
- end
- end
-end
diff --git a/lib/ci/migrate/database.rb b/lib/ci/migrate/database.rb
deleted file mode 100644
index bf9b80f1f62..00000000000
--- a/lib/ci/migrate/database.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'yaml'
-
-module Ci
- module Migrate
- class Database
- attr_reader :config
-
- def initialize
- @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
- end
-
- def restore
- decompress_rd, decompress_wr = IO.pipe
- decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
- decompress_wr.close
-
- restore_pid = case config["adapter"]
- when /^mysql/ then
- $progress.print "Restoring MySQL database #{config['database']} ... "
- # Workaround warnings from MySQL 5.6 about passwords on cmd line
- ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
- when "postgresql" then
- $progress.print "Restoring PostgreSQL database #{config['database']} ... "
- pg_env
- spawn('psql', config['database'], in: decompress_rd)
- end
- decompress_rd.close
-
- success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
- abort 'Restore failed' unless success
- end
-
- protected
-
- def db_file_name
- File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
- end
-
- def mysql_args
- args = {
- 'host' => '--host',
- 'port' => '--port',
- 'socket' => '--socket',
- 'username' => '--user',
- 'encoding' => '--default-character-set'
- }
- args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
- end
-
- def pg_env
- ENV['PGUSER'] = config["username"] if config["username"]
- ENV['PGHOST'] = config["host"] if config["host"]
- ENV['PGPORT'] = config["port"].to_s if config["port"]
- ENV['PGPASSWORD'] = config["password"].to_s if config["password"]
- end
-
- def report_success(success)
- if success
- puts '[DONE]'.green
- else
- puts '[FAILED]'.red
- end
- end
- end
- end
-end
diff --git a/lib/ci/migrate/manager.rb b/lib/ci/migrate/manager.rb
deleted file mode 100644
index e5e4fb784eb..00000000000
--- a/lib/ci/migrate/manager.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-module Ci
- module Migrate
- class Manager
- CI_IMPORT_PREFIX = '8.0' # Only allow imports from CI 8.0.x
-
- def cleanup
- $progress.print "Deleting tmp directories ... "
-
- backup_contents.each do |dir|
- next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
-
- if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir))
- $progress.puts "done".green
- else
- puts "deleting tmp directory '#{dir}' failed".red
- abort 'Backup failed'
- end
- end
- end
-
- def unpack
- Dir.chdir(Gitlab.config.backup.path)
-
- # check for existing backups in the backup dir
- file_list = Dir.glob("*_gitlab_ci_backup.tar").each.map { |f| f.split(/_/).first.to_i }
- puts "no backups found" if file_list.count == 0
-
- if file_list.count > 1 && ENV["BACKUP"].nil?
- puts "Found more than one backup, please specify which one you want to restore:"
- puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup"
- exit 1
- end
-
- tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_ci_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_ci_backup.tar")
-
- unless File.exists?(tar_file)
- puts "The specified CI backup doesn't exist!"
- exit 1
- end
-
- $progress.print "Unpacking backup ... "
-
- unless Kernel.system(*%W(tar -xf #{tar_file}))
- puts "unpacking backup failed".red
- exit 1
- else
- $progress.puts "done".green
- end
-
- ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
-
- # restoring mismatching backups can lead to unexpected problems
- if !settings[:gitlab_version].start_with?(CI_IMPORT_PREFIX)
- puts "GitLab CI version mismatch:".red
- puts " Your current GitLab CI version (#{GitlabCi::VERSION}) differs from the GitLab CI (#{settings[:gitlab_version]}) version in the backup!".red
- exit 1
- end
- end
-
- private
-
- def backup_contents
- ["db", "builds", "backup_information.yml"]
- end
-
- def settings
- @settings ||= YAML.load_file("backup_information.yml")
- end
- end
- end
-end
-
diff --git a/lib/ci/migrate/tags.rb b/lib/ci/migrate/tags.rb
deleted file mode 100644
index 97e043ece27..00000000000
--- a/lib/ci/migrate/tags.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'yaml'
-
-module Ci
- module Migrate
- class Tags
- def restore
- puts 'Inserting tags...'
- connection.select_all('SELECT ci_tags.name FROM ci_tags').each do |tag|
- begin
- connection.execute("INSERT INTO tags (name) VALUES(#{ActiveRecord::Base::sanitize(tag['name'])})")
- rescue ActiveRecord::RecordNotUnique
- end
- end
-
- ActiveRecord::Base.transaction do
- puts 'Deleting old taggings...'
- connection.execute "DELETE FROM taggings WHERE context = 'tags' AND taggable_type LIKE 'Ci::%'"
-
- puts 'Inserting taggings...'
- connection.execute(
- 'INSERT INTO taggings (taggable_type, taggable_id, tag_id, context) ' +
- "SELECT CONCAT('Ci::', ci_taggings.taggable_type), ci_taggings.taggable_id, tags.id, 'tags' FROM ci_taggings " +
- 'JOIN ci_tags ON ci_tags.id = ci_taggings.tag_id ' +
- 'JOIN tags ON tags.name = ci_tags.name '
- )
-
- puts 'Resetting counters... '
- connection.execute(
- 'UPDATE tags SET ' +
- 'taggings_count = (SELECT COUNT(*) FROM taggings WHERE tags.id = taggings.tag_id)'
- )
- end
- end
-
- protected
-
- def connection
- ActiveRecord::Base.connection
- end
- end
- end
-end
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index 6830a916bcb..85a2d1a93a7 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -193,12 +193,19 @@ module Grack
end
def render_grack_auth_ok
+ repo_path =
+ if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/
+ ProjectWiki.new(project).repository.path_to_repo
+ else
+ project.repository.path_to_repo
+ end
+
[
200,
{ "Content-Type" => "application/json" },
[JSON.dump({
'GL_ID' => Gitlab::ShellEnv.gl_id(@user),
- 'RepoPath' => project.repository.path_to_repo,
+ 'RepoPath' => repo_path,
})]
]
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 741a52714ac..71f37f1fef8 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -1,7 +1,7 @@
module Gitlab
module Database
def self.mysql?
- ActiveRecord::Base.connection.adapter_name.downcase == 'mysql'
+ ActiveRecord::Base.connection.adapter_name.downcase == 'mysql2'
end
def self.postgresql?
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0dab7bcfa4d..0a2be605af9 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -9,7 +9,7 @@ module Gitlab
else
nil
end
- @query = Shellwords.shellescape(query) if query.present?
+ @query = query
end
def objects(scope, page = nil)
diff --git a/lib/tasks/ci/migrate.rake b/lib/tasks/ci/migrate.rake
deleted file mode 100644
index 1de664c85e1..00000000000
--- a/lib/tasks/ci/migrate.rake
+++ /dev/null
@@ -1,87 +0,0 @@
-namespace :ci do
- desc 'GitLab | Import and migrate CI database'
- task migrate: :environment do
- warn_user_is_not_gitlab
- configure_cron_mode
-
- unless ENV['force'] == 'yes'
- puts 'This will remove all CI related data and restore it from the provided backup.'
- ask_to_continue
- puts ''
- end
-
- # disable CI for time of migration
- enable_ci(false)
-
- # unpack archives
- migrate = Ci::Migrate::Manager.new
- migrate.unpack
-
- Rake::Task['ci:migrate:db'].invoke
- Rake::Task['ci:migrate:builds'].invoke
- Rake::Task['ci:migrate:tags'].invoke
- Rake::Task['ci:migrate:services'].invoke
-
- # enable CI for time of migration
- enable_ci(true)
-
- migrate.cleanup
- end
-
- namespace :migrate do
- desc 'GitLab | Import CI database'
- task db: :environment do
- configure_cron_mode
- $progress.puts 'Restoring database ... '.blue
- Ci::Migrate::Database.new.restore
- $progress.puts 'done'.green
- end
-
- desc 'GitLab | Import CI builds'
- task builds: :environment do
- configure_cron_mode
- $progress.puts 'Restoring builds ... '.blue
- Ci::Migrate::Builds.new.restore
- $progress.puts 'done'.green
- end
-
- desc 'GitLab | Migrate CI tags'
- task tags: :environment do
- configure_cron_mode
- $progress.puts 'Migrating tags ... '.blue
- ::Ci::Migrate::Tags.new.restore
- $progress.puts 'done'.green
- end
-
- desc 'GitLab | Migrate CI auto-increments'
- task autoincrements: :environment do
- c = ActiveRecord::Base.connection
- c.tables.select { |t| t.start_with?('ci_') }.each do |table|
- result = c.select_one("SELECT id FROM #{table} ORDER BY id DESC LIMIT 1")
- if result
- ai_val = result['id'].to_i + 1
- puts "Resetting auto increment ID for #{table} to #{ai_val}"
- if c.adapter_name == 'PostgreSQL'
- c.execute("ALTER SEQUENCE #{table}_id_seq RESTART WITH #{ai_val}")
- else
- c.execute("ALTER TABLE #{table} AUTO_INCREMENT = #{ai_val}")
- end
- end
- end
- end
-
- desc 'GitLab | Migrate CI services'
- task services: :environment do
- $progress.puts 'Migrating services ... '.blue
- c = ActiveRecord::Base.connection
- c.execute("UPDATE ci_services SET type=CONCAT('Ci::', type) WHERE type NOT LIKE 'Ci::%'")
- $progress.puts 'done'.green
- end
- end
-
- def enable_ci(enabled)
- settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults
- settings.ci_enabled = enabled
- settings.save!
- end
-end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 4460bf12f96..4bb47c6b025 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -51,16 +51,39 @@ describe ProjectsController do
end
context "when requested with case sensitive namespace and project path" do
- it "redirects to the normalized path for case mismatch" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+ context "when there is a match with the same casing" do
+ it "loads the project" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
- expect(response).to redirect_to("/#{public_project.path_with_namespace}")
+ expect(assigns(:project)).to eq(public_project)
+ expect(response.status).to eq(200)
+ end
end
- it "loads the page if normalized path matches request path" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ context "when there is a match with different casing" do
+ it "redirects to the normalized path" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+
+ expect(assigns(:project)).to eq(public_project)
+ expect(response).to redirect_to("/#{public_project.path_with_namespace}")
+ end
+
- expect(response.status).to eq(200)
+ # MySQL queries are case insensitive by default, so this spec would fail.
+ if Gitlab::Database.postgresql?
+ context "when there is also a match with the same casing" do
+
+ let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) }
+
+ it "loads the exactly matched project" do
+
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+
+ expect(assigns(:project)).to eq(other_project)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 2260a6f8130..abdb6b89ac5 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -218,6 +218,20 @@ module Ci
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string")
end
+ it "returns errors if job name is blank" do
+ config = YAML.dump({ '' => { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end
+
+ it "returns errors if job name is non-string" do
+ config = YAML.dump({ 10 => { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end
+
it "returns errors if job image parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
expect do
diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb
index 37c527221a0..dfa0e10318a 100644
--- a/spec/lib/gitlab/backend/grack_auth_spec.rb
+++ b/spec/lib/gitlab/backend/grack_auth_spec.rb
@@ -50,6 +50,22 @@ describe Grack::Auth do
end
end
+ context "when the Wiki for a project exists" do
+ before do
+ @wiki = ProjectWiki.new(project)
+ env["PATH_INFO"] = "#{@wiki.repository.path_with_namespace}.git/info/refs"
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+ end
+
+ it "responds with the right project" do
+ response = auth.call(env)
+ json_body = ActiveSupport::JSON.decode(response[2][0])
+
+ expect(response.first).to eq(200)
+ expect(json_body['RepoPath']).to include(@wiki.repository.path_with_namespace)
+ end
+ end
+
context "when the project exists" do
before do
env["PATH_INFO"] = project.path_with_namespace + ".git"
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 32a25f08cac..19327ac8ce0 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
- it { expect(results.query).to eq('hello\\ world') }
+ it { expect(results.query).to eq('hello world') }
end
describe 'initialize with ref' do
@@ -18,6 +18,6 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
- it { expect(results.query).to eq('hello\\ world') }
+ it { expect(results.query).to eq('hello world') }
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 6aaf1c036b0..eed2cbc5412 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -79,6 +79,12 @@ describe MergeRequest do
expect(merge_request.commits).not_to be_empty
expect(merge_request.mr_and_commit_notes.count).to eq(2)
end
+
+ it "should include notes for commits from target project as well" do
+ create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.target_project)
+ expect(merge_request.commits).not_to be_empty
+ expect(merge_request.mr_and_commit_notes.count).to eq(3)
+ end
end
describe '#is_being_reassigned?' do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 4048c297013..0c19094ec54 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe API, api: true do
- include API::APIHelpers
+ include API::Helpers
include ApiHelpers
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -13,25 +13,25 @@ describe API, api: true do
def set_env(token_usr, identifier)
clear_env
clear_param
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
- env[API::APIHelpers::SUDO_HEADER] = identifier
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
+ env[API::Helpers::SUDO_HEADER] = identifier
end
def set_param(token_usr, identifier)
clear_env
clear_param
- params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
- params[API::APIHelpers::SUDO_PARAM] = identifier
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
+ params[API::Helpers::SUDO_PARAM] = identifier
end
def clear_env
- env.delete(API::APIHelpers::PRIVATE_TOKEN_HEADER)
- env.delete(API::APIHelpers::SUDO_HEADER)
+ env.delete(API::Helpers::PRIVATE_TOKEN_HEADER)
+ env.delete(API::Helpers::SUDO_HEADER)
end
def clear_param
- params.delete(API::APIHelpers::PRIVATE_TOKEN_PARAM)
- params.delete(API::APIHelpers::SUDO_PARAM)
+ params.delete(API::Helpers::PRIVATE_TOKEN_PARAM)
+ params.delete(API::Helpers::SUDO_PARAM)
end
def error!(message, status)
@@ -40,22 +40,22 @@ describe API, api: true do
describe ".current_user" do
it "should return nil for an invalid token" do
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
expect(current_user).to be_nil
end
it "should return nil for a user without access" do
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil
end
it "should leave user as is when sudo not specified" do
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
expect(current_user).to eq(user)
clear_env
- params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
expect(current_user).to eq(user)
end
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index d7242d684c6..cda7d0c4a51 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -4,8 +4,9 @@ module Ci
describe ImageForBuildService do
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:ci_project) }
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project, ref: 'master') }
+ let(:gl_project) { FactoryGirl.create(:project, gitlab_ci_project: project) }
+ let(:commit_sha) { gl_project.commit('master').sha }
+ let(:commit) { gl_project.ensure_ci_commit(commit_sha) }
let(:build) { FactoryGirl.create(:ci_build, commit: commit) }
describe :execute do
diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js
new file mode 100644
index 00000000000..1b1f4f0bd63
--- /dev/null
+++ b/vendor/assets/javascripts/clipboard.js
@@ -0,0 +1,621 @@
+/*!
+ * clipboard.js v1.4.2
+ * https://zenorocha.github.io/clipboard.js
+ *
+ * Licensed MIT © Zeno Rocha
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/**
+ * Module dependencies.
+ */
+
+var closest = require('closest')
+ , event = require('component-event');
+
+/**
+ * Delegate event `type` to `selector`
+ * and invoke `fn(e)`. A callback function
+ * is returned which may be passed to `.unbind()`.
+ *
+ * @param {Element} el
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+// Some events don't bubble, so we want to bind to the capture phase instead
+// when delegating.
+var forceCaptureEvents = ['focus', 'blur'];
+
+exports.bind = function(el, selector, type, fn, capture){
+ if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
+
+ return event.bind(el, type, function(e){
+ var target = e.target || e.srcElement;
+ e.delegateTarget = closest(target, selector, true, el);
+ if (e.delegateTarget) fn.call(el, e);
+ }, capture);
+};
+
+/**
+ * Unbind event `type`'s callback `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @api public
+ */
+
+exports.unbind = function(el, type, fn, capture){
+ if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
+
+ event.unbind(el, type, fn, capture);
+};
+
+},{"closest":2,"component-event":4}],2:[function(require,module,exports){
+var matches = require('matches-selector')
+
+module.exports = function (element, selector, checkYoSelf) {
+ var parent = checkYoSelf ? element : element.parentNode
+
+ while (parent && parent !== document) {
+ if (matches(parent, selector)) return parent;
+ parent = parent.parentNode
+ }
+}
+
+},{"matches-selector":3}],3:[function(require,module,exports){
+
+/**
+ * Element prototype.
+ */
+
+var proto = Element.prototype;
+
+/**
+ * Vendor function.
+ */
+
+var vendor = proto.matchesSelector
+ || proto.webkitMatchesSelector
+ || proto.mozMatchesSelector
+ || proto.msMatchesSelector
+ || proto.oMatchesSelector;
+
+/**
+ * Expose `match()`.
+ */
+
+module.exports = match;
+
+/**
+ * Match `el` to `selector`.
+ *
+ * @param {Element} el
+ * @param {String} selector
+ * @return {Boolean}
+ * @api public
+ */
+
+function match(el, selector) {
+ if (vendor) return vendor.call(el, selector);
+ var nodes = el.parentNode.querySelectorAll(selector);
+ for (var i = 0; i < nodes.length; ++i) {
+ if (nodes[i] == el) return true;
+ }
+ return false;
+}
+},{}],4:[function(require,module,exports){
+var bind = window.addEventListener ? 'addEventListener' : 'attachEvent',
+ unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent',
+ prefix = bind !== 'addEventListener' ? 'on' : '';
+
+/**
+ * Bind `el` event `type` to `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+exports.bind = function(el, type, fn, capture){
+ el[bind](prefix + type, fn, capture || false);
+ return fn;
+};
+
+/**
+ * Unbind `el` event `type`'s callback `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+exports.unbind = function(el, type, fn, capture){
+ el[unbind](prefix + type, fn, capture || false);
+ return fn;
+};
+},{}],5:[function(require,module,exports){
+function E () {
+ // Keep this empty so it's easier to inherit from
+ // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
+}
+
+E.prototype = {
+ on: function (name, callback, ctx) {
+ var e = this.e || (this.e = {});
+
+ (e[name] || (e[name] = [])).push({
+ fn: callback,
+ ctx: ctx
+ });
+
+ return this;
+ },
+
+ once: function (name, callback, ctx) {
+ var self = this;
+ var fn = function () {
+ self.off(name, fn);
+ callback.apply(ctx, arguments);
+ };
+
+ return this.on(name, fn, ctx);
+ },
+
+ emit: function (name) {
+ var data = [].slice.call(arguments, 1);
+ var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
+ var i = 0;
+ var len = evtArr.length;
+
+ for (i; i < len; i++) {
+ evtArr[i].fn.apply(evtArr[i].ctx, data);
+ }
+
+ return this;
+ },
+
+ off: function (name, callback) {
+ var e = this.e || (this.e = {});
+ var evts = e[name];
+ var liveEvents = [];
+
+ if (evts && callback) {
+ for (var i = 0, len = evts.length; i < len; i++) {
+ if (evts[i].fn !== callback) liveEvents.push(evts[i]);
+ }
+ }
+
+ // Remove event from queue to prevent memory leak
+ // Suggested by https://github.com/lazd
+ // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
+
+ (liveEvents.length)
+ ? e[name] = liveEvents
+ : delete e[name];
+
+ return this;
+ }
+};
+
+module.exports = E;
+
+},{}],6:[function(require,module,exports){
+/**
+ * Inner class which performs selection from either `text` or `target`
+ * properties and then executes copy or cut operations.
+ */
+'use strict';
+
+exports.__esModule = true;
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+var ClipboardAction = (function () {
+ /**
+ * @param {Object} options
+ */
+
+ function ClipboardAction(options) {
+ _classCallCheck(this, ClipboardAction);
+
+ this.resolveOptions(options);
+ this.initSelection();
+ }
+
+ /**
+ * Defines base properties passed from constructor.
+ * @param {Object} options
+ */
+
+ ClipboardAction.prototype.resolveOptions = function resolveOptions() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+ this.action = options.action;
+ this.emitter = options.emitter;
+ this.target = options.target;
+ this.text = options.text;
+ this.trigger = options.trigger;
+
+ this.selectedText = '';
+ };
+
+ /**
+ * Decides which selection strategy is going to be applied based
+ * on the existence of `text` and `target` properties.
+ */
+
+ ClipboardAction.prototype.initSelection = function initSelection() {
+ if (this.text && this.target) {
+ throw new Error('Multiple attributes declared, use either "target" or "text"');
+ } else if (this.text) {
+ this.selectFake();
+ } else if (this.target) {
+ this.selectTarget();
+ } else {
+ throw new Error('Missing required attributes, use either "target" or "text"');
+ }
+ };
+
+ /**
+ * Creates a fake textarea element, sets its value from `text` property,
+ * and makes a selection on it.
+ */
+
+ ClipboardAction.prototype.selectFake = function selectFake() {
+ var _this = this;
+
+ this.removeFake();
+
+ this.fakeHandler = document.body.addEventListener('click', function () {
+ return _this.removeFake();
+ });
+
+ this.fakeElem = document.createElement('textarea');
+ this.fakeElem.style.position = 'absolute';
+ this.fakeElem.style.left = '-9999px';
+ this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
+ this.fakeElem.setAttribute('readonly', '');
+ this.fakeElem.value = this.text;
+ this.selectedText = this.text;
+
+ document.body.appendChild(this.fakeElem);
+
+ this.fakeElem.select();
+ this.copyText();
+ };
+
+ /**
+ * Only removes the fake element after another click event, that way
+ * a user can hit `Ctrl+C` to copy because selection still exists.
+ */
+
+ ClipboardAction.prototype.removeFake = function removeFake() {
+ if (this.fakeHandler) {
+ document.body.removeEventListener('click');
+ this.fakeHandler = null;
+ }
+
+ if (this.fakeElem) {
+ document.body.removeChild(this.fakeElem);
+ this.fakeElem = null;
+ }
+ };
+
+ /**
+ * Selects the content from element passed on `target` property.
+ */
+
+ ClipboardAction.prototype.selectTarget = function selectTarget() {
+ if (this.target.nodeName === 'INPUT' || this.target.nodeName === 'TEXTAREA') {
+ this.target.select();
+ this.selectedText = this.target.value;
+ } else {
+ var range = document.createRange();
+ var selection = window.getSelection();
+
+ selection.removeAllRanges();
+ range.selectNodeContents(this.target);
+ selection.addRange(range);
+ this.selectedText = selection.toString();
+ }
+
+ this.copyText();
+ };
+
+ /**
+ * Executes the copy operation based on the current selection.
+ */
+
+ ClipboardAction.prototype.copyText = function copyText() {
+ var succeeded = undefined;
+
+ try {
+ succeeded = document.execCommand(this.action);
+ } catch (err) {
+ succeeded = false;
+ }
+
+ this.handleResult(succeeded);
+ };
+
+ /**
+ * Fires an event based on the copy operation result.
+ * @param {Boolean} succeeded
+ */
+
+ ClipboardAction.prototype.handleResult = function handleResult(succeeded) {
+ if (succeeded) {
+ this.emitter.emit('success', {
+ action: this.action,
+ text: this.selectedText,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ } else {
+ this.emitter.emit('error', {
+ action: this.action,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ }
+ };
+
+ /**
+ * Removes current selection and focus from `target` element.
+ */
+
+ ClipboardAction.prototype.clearSelection = function clearSelection() {
+ if (this.target) {
+ this.target.blur();
+ }
+
+ window.getSelection().removeAllRanges();
+ };
+
+ /**
+ * Sets the `action` to be performed which can be either 'copy' or 'cut'.
+ * @param {String} action
+ */
+
+ /**
+ * Destroy lifecycle.
+ */
+
+ ClipboardAction.prototype.destroy = function destroy() {
+ this.removeFake();
+ };
+
+ _createClass(ClipboardAction, [{
+ key: 'action',
+ set: function set() {
+ var action = arguments.length <= 0 || arguments[0] === undefined ? 'copy' : arguments[0];
+
+ this._action = action;
+
+ if (this._action !== 'copy' && this._action !== 'cut') {
+ throw new Error('Invalid "action" value, use either "copy" or "cut"');
+ }
+ },
+
+ /**
+ * Gets the `action` property.
+ * @return {String}
+ */
+ get: function get() {
+ return this._action;
+ }
+
+ /**
+ * Sets the `target` property using an element
+ * that will be have its content copied.
+ * @param {Element} target
+ */
+ }, {
+ key: 'target',
+ set: function set(target) {
+ if (target !== undefined) {
+ if (target && typeof target === 'object' && target.nodeType === 1) {
+ this._target = target;
+ } else {
+ throw new Error('Invalid "target" value, use a valid Element');
+ }
+ }
+ },
+
+ /**
+ * Gets the `target` property.
+ * @return {String|HTMLElement}
+ */
+ get: function get() {
+ return this._target;
+ }
+ }]);
+
+ return ClipboardAction;
+})();
+
+exports['default'] = ClipboardAction;
+module.exports = exports['default'];
+
+},{}],7:[function(require,module,exports){
+'use strict';
+
+exports.__esModule = true;
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _clipboardAction = require('./clipboard-action');
+
+var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
+
+var _delegateEvents = require('delegate-events');
+
+var _delegateEvents2 = _interopRequireDefault(_delegateEvents);
+
+var _tinyEmitter = require('tiny-emitter');
+
+var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
+
+/**
+ * Base class which takes a selector, delegates a click event to it,
+ * and instantiates a new `ClipboardAction` on each click.
+ */
+
+var Clipboard = (function (_Emitter) {
+ _inherits(Clipboard, _Emitter);
+
+ /**
+ * @param {String} selector
+ * @param {Object} options
+ */
+
+ function Clipboard(selector, options) {
+ _classCallCheck(this, Clipboard);
+
+ _Emitter.call(this);
+
+ this.resolveOptions(options);
+ this.delegateClick(selector);
+ }
+
+ /**
+ * Helper function to retrieve attribute value.
+ * @param {String} suffix
+ * @param {Element} element
+ */
+
+ /**
+ * Defines if attributes would be resolved using internal setter functions
+ * or custom functions that were passed in the constructor.
+ * @param {Object} options
+ */
+
+ Clipboard.prototype.resolveOptions = function resolveOptions() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+ this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
+ this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
+ this.text = typeof options.text === 'function' ? options.text : this.defaultText;
+ };
+
+ /**
+ * Delegates a click event on the passed selector.
+ * @param {String} selector
+ */
+
+ Clipboard.prototype.delegateClick = function delegateClick(selector) {
+ var _this = this;
+
+ this.binding = _delegateEvents2['default'].bind(document.body, selector, 'click', function (e) {
+ return _this.onClick(e);
+ });
+ };
+
+ /**
+ * Undelegates a click event on body.
+ * @param {String} selector
+ */
+
+ Clipboard.prototype.undelegateClick = function undelegateClick() {
+ _delegateEvents2['default'].unbind(document.body, 'click', this.binding);
+ };
+
+ /**
+ * Defines a new `ClipboardAction` on each click event.
+ * @param {Event} e
+ */
+
+ Clipboard.prototype.onClick = function onClick(e) {
+ if (this.clipboardAction) {
+ this.clipboardAction = null;
+ }
+
+ this.clipboardAction = new _clipboardAction2['default']({
+ action: this.action(e.delegateTarget),
+ target: this.target(e.delegateTarget),
+ text: this.text(e.delegateTarget),
+ trigger: e.delegateTarget,
+ emitter: this
+ });
+ };
+
+ /**
+ * Default `action` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultAction = function defaultAction(trigger) {
+ return getAttributeValue('action', trigger);
+ };
+
+ /**
+ * Default `target` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultTarget = function defaultTarget(trigger) {
+ var selector = getAttributeValue('target', trigger);
+
+ if (selector) {
+ return document.querySelector(selector);
+ }
+ };
+
+ /**
+ * Default `text` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultText = function defaultText(trigger) {
+ return getAttributeValue('text', trigger);
+ };
+
+ /**
+ * Destroy lifecycle.
+ */
+
+ Clipboard.prototype.destroy = function destroy() {
+ this.undelegateClick();
+
+ if (this.clipboardAction) {
+ this.clipboardAction.destroy();
+ this.clipboardAction = null;
+ }
+ };
+
+ return Clipboard;
+})(_tinyEmitter2['default']);
+
+function getAttributeValue(suffix, element) {
+ var attribute = 'data-clipboard-' + suffix;
+
+ if (!element.hasAttribute(attribute)) {
+ return;
+ }
+
+ return element.getAttribute(attribute);
+}
+
+exports['default'] = Clipboard;
+module.exports = exports['default'];
+
+},{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
+}); \ No newline at end of file