From 2e6042f6024b5ad01bc3f62ec52f947cf0e8e8c5 Mon Sep 17 00:00:00 2001 From: Foteini Giannaropoulou Date: Wed, 23 Jun 2021 12:18:53 +0300 Subject: [PATCH 1/8] Jetpack Backup: Utilize endpoints from backup package (#20140) * Jetpack Backup: Utilize endpoints from backup package * Backup plugin: Add changelog --- .../update-backup-plugin-with-backup-pkg | 4 ++ projects/plugins/backup/composer.json | 1 + projects/plugins/backup/composer.lock | 63 ++++++++++++++++++- .../backup/src/php/class-jetpack-backup.php | 5 ++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 projects/plugins/backup/changelog/update-backup-plugin-with-backup-pkg diff --git a/projects/plugins/backup/changelog/update-backup-plugin-with-backup-pkg b/projects/plugins/backup/changelog/update-backup-plugin-with-backup-pkg new file mode 100644 index 0000000000000..bef157dbc0bc9 --- /dev/null +++ b/projects/plugins/backup/changelog/update-backup-plugin-with-backup-pkg @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Add backup package dependency. diff --git a/projects/plugins/backup/composer.json b/projects/plugins/backup/composer.json index 0d5cd5ab6f428..c43b59129d694 100644 --- a/projects/plugins/backup/composer.json +++ b/projects/plugins/backup/composer.json @@ -5,6 +5,7 @@ "license": "GPL-2.0-or-later", "require": { "automattic/jetpack-autoloader": "2.10.x-dev", + "automattic/jetpack-backup": "1.1.x-dev", "automattic/jetpack-config": "1.4.x-dev", "automattic/jetpack-connection": "1.28.x-dev", "automattic/jetpack-connection-ui": "1.2.x-dev", diff --git a/projects/plugins/backup/composer.lock b/projects/plugins/backup/composer.lock index ccb68ab2167e2..41c674de92540 100644 --- a/projects/plugins/backup/composer.lock +++ b/projects/plugins/backup/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1701fdfb9bd16deeb2df11f37417e8c1", + "content-hash": "74c52cf5e29b9385f38ed0b7ebbbb85c", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -168,6 +168,66 @@ "relative": true } }, + { + "name": "automattic/jetpack-backup", + "version": "dev-master", + "dist": { + "type": "path", + "url": "../../packages/backup", + "reference": "5fb371087dab728ea1188acfa3698dbfa2b7320c" + }, + "require": { + "automattic/jetpack-connection": "^1.28" + }, + "require-dev": { + "automattic/jetpack-changelogger": "^1.2", + "automattic/wordbless": "@dev", + "yoast/phpunit-polyfills": "0.2.0" + }, + "type": "library", + "extra": { + "autotagger": true, + "mirror-repo": "Automattic/jetpack-backup", + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-backup/compare/v${old}...v${new}" + }, + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "files": [ + "actions.php" + ], + "classmap": [ + "src/" + ] + }, + "scripts": { + "phpunit": [ + "@composer install", + "./vendor/phpunit/phpunit/phpunit --colors=always" + ], + "test-coverage": [ + "@composer install", + "phpdbg -d memory_limit=2048M -d max_execution_time=900 -qrr ./vendor/bin/phpunit --coverage-clover \"$COVERAGE_DIR/clover.xml\"" + ], + "test-php": [ + "@composer phpunit" + ], + "post-update-cmd": [ + "php -r \"copy('vendor/automattic/wordbless/src/dbless-wpdb.php', 'wordpress/wp-content/db.php');\"" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "Tools to assist with backing up Jetpack sites.", + "transport-options": { + "monorepo": true, + "relative": true + } + }, { "name": "automattic/jetpack-config", "version": "dev-master", @@ -4143,6 +4203,7 @@ "minimum-stability": "dev", "stability-flags": { "automattic/jetpack-autoloader": 20, + "automattic/jetpack-backup": 20, "automattic/jetpack-config": 20, "automattic/jetpack-connection": 20, "automattic/jetpack-connection-ui": 20, diff --git a/projects/plugins/backup/src/php/class-jetpack-backup.php b/projects/plugins/backup/src/php/class-jetpack-backup.php index b0d681abe8e90..fe66a1188f43c 100644 --- a/projects/plugins/backup/src/php/class-jetpack-backup.php +++ b/projects/plugins/backup/src/php/class-jetpack-backup.php @@ -9,6 +9,8 @@ exit; } +use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication; + /** * Class Jetpack_Backup */ @@ -17,6 +19,9 @@ class Jetpack_Backup { * Constructor. */ public function __construct() { + // Set up the REST authentication hooks. + Connection_Rest_Authentication::init(); + add_action( 'admin_menu', function () { From d8693f230f737c400a33fb7fd7e9cfabbe42782a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Gomes?= Date: Wed, 23 Jun 2021 12:06:01 +0100 Subject: [PATCH 2/8] Carousel: yet more resilience to missing data (#20149) It now uses the thumbnail dimensions when image sizes are missing. --- .../update-carousel-missing-attribute-resilience-part-2 | 4 ++++ projects/plugins/jetpack/modules/carousel/jetpack-carousel.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 projects/plugins/jetpack/changelog/update-carousel-missing-attribute-resilience-part-2 diff --git a/projects/plugins/jetpack/changelog/update-carousel-missing-attribute-resilience-part-2 b/projects/plugins/jetpack/changelog/update-carousel-missing-attribute-resilience-part-2 new file mode 100644 index 0000000000000..c95811764fc78 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-carousel-missing-attribute-resilience-part-2 @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +Carousel: add yet more resilience to missing image data. diff --git a/projects/plugins/jetpack/modules/carousel/jetpack-carousel.js b/projects/plugins/jetpack/modules/carousel/jetpack-carousel.js index bacd435ca4674..bc1fd3f075efb 100644 --- a/projects/plugins/jetpack/modules/carousel/jetpack-carousel.js +++ b/projects/plugins/jetpack/modules/carousel/jetpack-carousel.js @@ -1518,8 +1518,8 @@ var origDimensions = getOriginalDimensions( item ); - attrs.origWidth = origDimensions.width; - attrs.origHeight = origDimensions.height; + attrs.origWidth = origDimensions.width || attrs.thumbSize.width; + attrs.origHeight = origDimensions.height || attrs.thumbSize.height; if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) { attrs.src = wpcom.carousel.generateImgSrc( item, max ); From 6b32f6696acf1aea1244366ecabb45980152ce1e Mon Sep 17 00:00:00 2001 From: Miguel Torres Date: Wed, 23 Jun 2021 15:16:39 +0200 Subject: [PATCH 3/8] Admin menu: Register Calypso settings pages as independent submenus (#20100) --- .../update-admin-menu-calypso-settings-submenus | 4 ++++ .../masterbar/admin-menu/class-admin-menu.php | 9 +++++++-- .../admin-menu/class-atomic-admin-menu.php | 9 ++++++++- .../admin-menu/class-jetpack-admin-menu.php | 15 ++++++++++++++- .../admin-menu/class-wpcom-admin-menu.php | 2 +- .../modules/masterbar/test-class-admin-menu.php | 2 +- .../masterbar/test-class-atomic-admin-menu.php | 2 +- .../masterbar/test-class-wpcom-admin-menu.php | 2 +- 8 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 projects/plugins/jetpack/changelog/update-admin-menu-calypso-settings-submenus diff --git a/projects/plugins/jetpack/changelog/update-admin-menu-calypso-settings-submenus b/projects/plugins/jetpack/changelog/update-admin-menu-calypso-settings-submenus new file mode 100644 index 0000000000000..549dc5c915cff --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-admin-menu-calypso-settings-submenus @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Admin menu: Register Calypso settings pages as independent submenus diff --git a/projects/plugins/jetpack/modules/masterbar/admin-menu/class-admin-menu.php b/projects/plugins/jetpack/modules/masterbar/admin-menu/class-admin-menu.php index c97e43d5030b5..29d4b0374883d 100644 --- a/projects/plugins/jetpack/modules/masterbar/admin-menu/class-admin-menu.php +++ b/projects/plugins/jetpack/modules/masterbar/admin-menu/class-admin-menu.php @@ -430,12 +430,17 @@ public function add_options_menu( $wp_admin = false ) { $this->update_submenus( 'options-general.php', array( 'options-general.php' => 'https://wordpress.com/settings/general/' . $this->domain ) ); add_submenu_page( 'options-general.php', esc_attr__( 'Advanced General', 'jetpack' ), __( 'Advanced General', 'jetpack' ), 'manage_options', 'options-general.php', null, 1 ); + add_submenu_page( 'options-general.php', esc_attr__( 'Performance', 'jetpack' ), __( 'Performance', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/performance/' . $this->domain, null, 2 ); + if ( $wp_admin ) { return; } - $this->hide_submenu_page( 'options-general.php', 'options-discussion.php' ); - $this->hide_submenu_page( 'options-general.php', 'options-writing.php' ); + $submenus_to_update = array( + 'options-writing.php' => 'https://wordpress.com/settings/writing/' . $this->domain, + 'options-discussion.php' => 'https://wordpress.com/settings/discussion/' . $this->domain, + ); + $this->update_submenus( 'options-general.php', $submenus_to_update ); } /** diff --git a/projects/plugins/jetpack/modules/masterbar/admin-menu/class-atomic-admin-menu.php b/projects/plugins/jetpack/modules/masterbar/admin-menu/class-atomic-admin-menu.php index 47ce3c75f7b27..e895e56ce9b9b 100644 --- a/projects/plugins/jetpack/modules/masterbar/admin-menu/class-atomic-admin-menu.php +++ b/projects/plugins/jetpack/modules/masterbar/admin-menu/class-atomic-admin-menu.php @@ -292,7 +292,14 @@ public function add_tools_menu( $wp_admin_import = false, $wp_admin_export = fal public function add_options_menu( $wp_admin = false ) { parent::add_options_menu( $wp_admin ); - add_submenu_page( 'options-general.php', esc_attr__( 'Hosting Configuration', 'jetpack' ), __( 'Hosting Configuration', 'jetpack' ), 'manage_options', 'https://wordpress.com/hosting-config/' . $this->domain, null, 6 ); + add_submenu_page( 'options-general.php', esc_attr__( 'Security', 'jetpack' ), __( 'Security', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/security/' . $this->domain, null, 2 ); + add_submenu_page( 'options-general.php', esc_attr__( 'Hosting Configuration', 'jetpack' ), __( 'Hosting Configuration', 'jetpack' ), 'manage_options', 'https://wordpress.com/hosting-config/' . $this->domain, null, 11 ); + add_submenu_page( 'options-general.php', esc_attr__( 'Jetpack', 'jetpack' ), __( 'Jetpack', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/jetpack/' . $this->domain, 12 ); + + // Page Optimize is active by default on all Atomic sites and registers a Settings > Performance submenu which + // would conflict with our own Settings > Performance that links to Calypso, so we hide it it since the Calypso + // performance settings already have a link to Page Optimize settings page. + $this->hide_submenu_page( 'options-general.php', 'page-optimize' ); // No need to add a menu linking to WP Admin if there is already one. if ( ! $wp_admin ) { diff --git a/projects/plugins/jetpack/modules/masterbar/admin-menu/class-jetpack-admin-menu.php b/projects/plugins/jetpack/modules/masterbar/admin-menu/class-jetpack-admin-menu.php index 53a33d4cc8271..b3b1ea0aa5c92 100644 --- a/projects/plugins/jetpack/modules/masterbar/admin-menu/class-jetpack-admin-menu.php +++ b/projects/plugins/jetpack/modules/masterbar/admin-menu/class-jetpack-admin-menu.php @@ -222,7 +222,20 @@ public function add_tools_menu( $wp_admin_import = false, $wp_admin_export = fal * @param bool $wp_admin Optional. Whether links should point to Calypso or wp-admin. Default false (Calypso). */ public function add_options_menu( $wp_admin = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - add_menu_page( esc_attr__( 'Settings', 'jetpack' ), __( 'Settings', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/general/' . $this->domain, null, 'dashicons-admin-settings', 80 ); + $slug = 'https://wordpress.com/settings/general/' . $this->domain; + add_menu_page( esc_attr__( 'Settings', 'jetpack' ), __( 'Settings', 'jetpack' ), 'manage_options', $slug, null, 'dashicons-admin-settings', 80 ); + add_submenu_page( $slug, esc_attr__( 'General', 'jetpack' ), __( 'General', 'jetpack' ), 'manage_options', $slug ); + add_submenu_page( $slug, esc_attr__( 'Security', 'jetpack' ), __( 'Security', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/security/' . $this->domain ); + add_submenu_page( $slug, esc_attr__( 'Performance', 'jetpack' ), __( 'Performance', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/performance/' . $this->domain ); + add_submenu_page( $slug, esc_attr__( 'Writing', 'jetpack' ), __( 'Writing', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/writing/' . $this->domain ); + add_submenu_page( $slug, esc_attr__( 'Discussion', 'jetpack' ), __( 'Discussion', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/discussion/' . $this->domain ); + + $has_scan = \Jetpack_Plan::supports( 'scan' ); + $rewind_state = get_transient( 'jetpack_rewind_state' ); + $has_backup = $rewind_state && in_array( $rewind_state->state, array( 'awaiting_credentials', 'provisioning', 'active' ), true ); + if ( $has_scan || $has_backup ) { + add_submenu_page( $slug, esc_attr__( 'Jetpack', 'jetpack' ), __( 'Jetpack', 'jetpack' ), 'manage_options', 'https://wordpress.com/settings/jetpack/' . $this->domain ); + } } /** diff --git a/projects/plugins/jetpack/modules/masterbar/admin-menu/class-wpcom-admin-menu.php b/projects/plugins/jetpack/modules/masterbar/admin-menu/class-wpcom-admin-menu.php index 4060c2d3d676a..5a1b91225e041 100644 --- a/projects/plugins/jetpack/modules/masterbar/admin-menu/class-wpcom-admin-menu.php +++ b/projects/plugins/jetpack/modules/masterbar/admin-menu/class-wpcom-admin-menu.php @@ -306,7 +306,7 @@ public function add_users_menu( $wp_admin = false ) { // phpcs:ignore VariableAn public function add_options_menu( $wp_admin = false ) { parent::add_options_menu( $wp_admin ); - add_submenu_page( 'options-general.php', esc_attr__( 'Hosting Configuration', 'jetpack' ), __( 'Hosting Configuration', 'jetpack' ), 'manage_options', 'https://wordpress.com/hosting-config/' . $this->domain, null, 6 ); + add_submenu_page( 'options-general.php', esc_attr__( 'Hosting Configuration', 'jetpack' ), __( 'Hosting Configuration', 'jetpack' ), 'manage_options', 'https://wordpress.com/hosting-config/' . $this->domain, null, 10 ); } /** diff --git a/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-admin-menu.php b/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-admin-menu.php index d20637efa510b..cb949f2714d51 100644 --- a/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-admin-menu.php +++ b/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-admin-menu.php @@ -404,7 +404,7 @@ public function test_add_options_menu() { static::$admin_menu->add_options_menu(); - $this->assertSame( 'https://wordpress.com/settings/general/' . static::$domain, array_shift( $submenu['options-general.php'] )[2] ); + $this->assertSame( 'https://wordpress.com/settings/general/' . static::$domain, $submenu['options-general.php'][0][2] ); $this->assertSame( 'options-general.php', $submenu['options-general.php'][1][2] ); } diff --git a/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-atomic-admin-menu.php b/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-atomic-admin-menu.php index babe9c5270b68..3a44be7f0218e 100644 --- a/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-atomic-admin-menu.php +++ b/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-atomic-admin-menu.php @@ -335,7 +335,7 @@ public function test_add_options_menu() { global $submenu; static::$admin_menu->add_options_menu(); - $this->assertSame( 'https://wordpress.com/hosting-config/' . static::$domain, $submenu['options-general.php'][6][2] ); + $this->assertSame( 'https://wordpress.com/hosting-config/' . static::$domain, $submenu['options-general.php'][11][2] ); $this->assertSame( 'options-writing.php', array_pop( $submenu['options-general.php'] )[2] ); // Reset. diff --git a/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-wpcom-admin-menu.php b/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-wpcom-admin-menu.php index b969c413bb935..668df046f45d1 100644 --- a/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-wpcom-admin-menu.php +++ b/projects/plugins/jetpack/tests/php/modules/masterbar/test-class-wpcom-admin-menu.php @@ -313,7 +313,7 @@ public function test_add_options_menu() { static::$admin_menu->add_options_menu(); - $this->assertSame( 'https://wordpress.com/hosting-config/' . static::$domain, $submenu['options-general.php'][6][2] ); + $this->assertSame( 'https://wordpress.com/hosting-config/' . static::$domain, $submenu['options-general.php'][10][2] ); } /** From c242af3a71cfbe3c278ed13b08c2ce7d4d1b2a2d Mon Sep 17 00:00:00 2001 From: Yaroslav Kukharuk Date: Wed, 23 Jun 2021 16:26:41 +0300 Subject: [PATCH 4/8] E2E Tests: Increase in-place wait timeout (#20152) --- .../update-e2e-in-place-connection-timeout-increase | 5 +++++ .../tests/e2e/lib/pages/wp-admin/in-place-authorize.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 projects/plugins/jetpack/changelog/update-e2e-in-place-connection-timeout-increase diff --git a/projects/plugins/jetpack/changelog/update-e2e-in-place-connection-timeout-increase b/projects/plugins/jetpack/changelog/update-e2e-in-place-connection-timeout-increase new file mode 100644 index 0000000000000..93fdafea75f5e --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-e2e-in-place-connection-timeout-increase @@ -0,0 +1,5 @@ +Significance: patch +Type: other +Comment: small change in E2E timeout + + diff --git a/projects/plugins/jetpack/tests/e2e/lib/pages/wp-admin/in-place-authorize.js b/projects/plugins/jetpack/tests/e2e/lib/pages/wp-admin/in-place-authorize.js index 0b2f7eb523e53..7cbc7081627c4 100644 --- a/projects/plugins/jetpack/tests/e2e/lib/pages/wp-admin/in-place-authorize.js +++ b/projects/plugins/jetpack/tests/e2e/lib/pages/wp-admin/in-place-authorize.js @@ -35,6 +35,6 @@ export default class InPlaceAuthorizeFrame extends WpPage { } async waitToDisappear() { - return await this.waitForElementToBeHidden( this.selectors[ 0 ] ); + return await this.waitForElementToBeHidden( this.selectors[ 0 ], 40000 ); } } From d0e02444e09ffb24235bb1e4db5e4cc6c0415814 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Wed, 23 Jun 2021 10:16:06 -0400 Subject: [PATCH 5/8] eslint-changed: Make it a project (#20092) Finally got around to making this a project (and writing tests for it). Not figuring out how to push this to the NPM repo yet, but this is a step along that path. Additional changes: * Docs and tests! * Fix handling when files are specified on the command line, particularly for manual mode. * Show new errors/warnings even in lines outside the diff, e.g. where removing a `var` causes a `no-undef` elsewhere. Add a command line option for the old behavior, in case someone wants that. Co-authored-by: Jeremy Herve --- .github/renovate.json | 1 + package.json | 4 +- pnpm-lock.yaml | 22 +- .../js-packages/eslint-changed/.gitattributes | 5 + .../js-packages/eslint-changed/.gitignore | 1 + projects/js-packages/eslint-changed/.nycrc | 5 + .../js-packages/eslint-changed/CHANGELOG.md | 10 + projects/js-packages/eslint-changed/README.md | 87 ++ .../eslint-changed/bin}/eslint-changed.js | 72 +- .../eslint-changed/changelog/.gitkeep | 0 .../update-move-eslint-changed-to-project | 4 + .../js-packages/eslint-changed/composer.json | 33 + .../js-packages/eslint-changed/license.txt | 357 ++++++++ .../js-packages/eslint-changed/package.json | 39 + .../tests/bin/eslint-changed.js | 792 ++++++++++++++++++ .../tests/fixtures/files-123.diff | 15 + .../tests/fixtures/files-123.expect.json | 68 ++ .../tests/fixtures/files-123.new.json | 68 ++ .../tests/fixtures/files-123.orig.json | 29 + .../tests/fixtures/manual-mode.diff | 9 + .../tests/fixtures/manual-mode.expect.json | 34 + .../tests/fixtures/manual-mode.new.json | 58 ++ .../tests/fixtures/manual-mode.orig.json | 70 ++ .../tests/fixtures/new-err-not-in-diff.diff | 9 + .../fixtures/new-err-not-in-diff.expect.json | 24 + .../fixtures/new-err-not-in-diff.expect2.json | 12 + .../fixtures/new-err-not-in-diff.new.json | 36 + .../fixtures/new-err-not-in-diff.orig.json | 25 + .../tests/fixtures/no-new-errors.diff | 10 + .../tests/fixtures/no-new-errors.expect.json | 21 + .../tests/fixtures/no-new-errors.new.json | 46 + .../tests/fixtures/no-new-errors.orig.json | 70 ++ 32 files changed, 2008 insertions(+), 28 deletions(-) create mode 100644 projects/js-packages/eslint-changed/.gitattributes create mode 100644 projects/js-packages/eslint-changed/.gitignore create mode 100644 projects/js-packages/eslint-changed/.nycrc create mode 100644 projects/js-packages/eslint-changed/CHANGELOG.md create mode 100644 projects/js-packages/eslint-changed/README.md rename {tools => projects/js-packages/eslint-changed/bin}/eslint-changed.js (80%) create mode 100644 projects/js-packages/eslint-changed/changelog/.gitkeep create mode 100644 projects/js-packages/eslint-changed/changelog/update-move-eslint-changed-to-project create mode 100644 projects/js-packages/eslint-changed/composer.json create mode 100644 projects/js-packages/eslint-changed/license.txt create mode 100644 projects/js-packages/eslint-changed/package.json create mode 100644 projects/js-packages/eslint-changed/tests/bin/eslint-changed.js create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/files-123.diff create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/files-123.expect.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/files-123.new.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/files-123.orig.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/manual-mode.diff create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/manual-mode.expect.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/manual-mode.new.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/manual-mode.orig.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.diff create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.expect.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.expect2.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.new.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.orig.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.diff create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.expect.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.new.json create mode 100644 projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.orig.json diff --git a/.github/renovate.json b/.github/renovate.json index 961e4151f4780..059c7ec4086b2 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,6 +12,7 @@ { "groupName": "Monorepo packages", "matchPackageNames": [ + "@automattic/eslint-changed", "@automattic/jetpack-connection", "automattic/jetpack-a8c-mc-stats", "automattic/jetpack-abtest", diff --git a/package.json b/package.json index d007d4fe4ec09..410799bf9c3d7 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "docker:wp": "printf '\\e[30;43m`pnpm run docker:wp` is deprected. Use the Jetpack CLI instead.\\e[0m\\n'; jetpack docker wp", "install-if-deps-outdated": "pnpm install --no-prod --frozen-lockfile", "lint": "pnpm run lint-file -- .", - "lint-changed": "tools/eslint-changed.js --ext .js,.jsx --git", + "lint-changed": "eslint-changed --ext .js,.jsx --git", "lint-file": "eslint --ext .js,.jsx", "lint-required": "node -e \"const fs = require('fs'); fs.copyFileSync('.eslintignore','.eslintignore-required'); const w=fs.createWriteStream('.eslintignore-required',{flags:'a'}); w.write('\\n# tools/eslint-excludelist.json\\n'); w.end(JSON.parse(fs.readFileSync('tools/eslint-excludelist.json','utf8')).join('\\n')+'\\n')\" && pnpm run lint -- --max-warnings=0 --ignore-path .eslintignore-required", "php:autofix": "composer phpcs:fix", @@ -84,9 +84,9 @@ "chalk": "4.1.1" }, "devDependencies": { + "@automattic/eslint-changed": "workspace:1.0.1-alpha", "@wordpress/eslint-plugin": "7.4.0", "babel-eslint": "10.1.0", - "commander": "7.2.0", "eslint": "7.25.0", "eslint-config-prettier": "8.3.0", "eslint-config-wpcalypso": "6.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61f5f64494e0f..9fc283df1e59c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,10 +12,10 @@ importers: .: specifiers: + '@automattic/eslint-changed': workspace:1.0.1-alpha '@wordpress/eslint-plugin': 7.4.0 babel-eslint: 10.1.0 chalk: 4.1.1 - commander: 7.2.0 eslint: 7.25.0 eslint-config-prettier: 8.3.0 eslint-config-wpcalypso: 6.1.0 @@ -39,9 +39,9 @@ importers: dependencies: chalk: 4.1.1 devDependencies: + '@automattic/eslint-changed': link:projects/js-packages/eslint-changed '@wordpress/eslint-plugin': 7.4.0_eslint@7.25.0 babel-eslint: 10.1.0_eslint@7.25.0 - commander: 7.2.0 eslint: 7.25.0 eslint-config-prettier: 8.3.0_eslint@7.25.0 eslint-config-wpcalypso: 6.1.0_6d3db79ccf2b505de9219d0afc266b20 @@ -124,6 +124,23 @@ importers: react-dom: 16.14.0_react@16.14.0 react-test-renderer: 16.14.0_react@16.14.0 + projects/js-packages/eslint-changed: + specifiers: + chalk: 4.1.1 + commander: 7.2.0 + eslint: 7.25.0 + jetpack-js-test-runner: workspace:* + nyc: 15.1.0 + parse-diff: 0.8.1 + dependencies: + chalk: 4.1.1 + commander: 7.2.0 + parse-diff: 0.8.1 + devDependencies: + eslint: 7.25.0 + jetpack-js-test-runner: link:../../../tools/js-test-runner + nyc: 15.1.0 + projects/packages/connection-ui: specifiers: '@automattic/calypso-build': 6.5.0 @@ -13381,7 +13398,6 @@ packages: /parse-diff/0.8.1: resolution: {integrity: sha512-0QG0HqwXCC/zMohOlaxkQmV1igZq1LQ6xsv/ziex6TDbY0GFxr3TDJN+/aHjWH3s2WTysSW3Bhs9Yfh6DOelFA==} - dev: true /parse-filepath/1.0.2: resolution: {integrity: sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=} diff --git a/projects/js-packages/eslint-changed/.gitattributes b/projects/js-packages/eslint-changed/.gitattributes new file mode 100644 index 0000000000000..3af07a98de415 --- /dev/null +++ b/projects/js-packages/eslint-changed/.gitattributes @@ -0,0 +1,5 @@ +/changelog/** production-exclude +/.gitattributes production-exclude +/.gitignore production-exclude +/.nycrc production-exclude +/tests/** production-exclude diff --git a/projects/js-packages/eslint-changed/.gitignore b/projects/js-packages/eslint-changed/.gitignore new file mode 100644 index 0000000000000..1c00193f5a671 --- /dev/null +++ b/projects/js-packages/eslint-changed/.gitignore @@ -0,0 +1 @@ +/.nyc_output/ diff --git a/projects/js-packages/eslint-changed/.nycrc b/projects/js-packages/eslint-changed/.nycrc new file mode 100644 index 0000000000000..d66a25f800654 --- /dev/null +++ b/projects/js-packages/eslint-changed/.nycrc @@ -0,0 +1,5 @@ +{ + "extensions": [".js"], + "exclude": "tests/", + "reporter": "clover" +} diff --git a/projects/js-packages/eslint-changed/CHANGELOG.md b/projects/js-packages/eslint-changed/CHANGELOG.md new file mode 100644 index 0000000000000..a86d7c3d0e565 --- /dev/null +++ b/projects/js-packages/eslint-changed/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 1.0.0 - unreleased + +* Created as a tool within the monorepo. diff --git a/projects/js-packages/eslint-changed/README.md b/projects/js-packages/eslint-changed/README.md new file mode 100644 index 0000000000000..9913ad5a2add9 --- /dev/null +++ b/projects/js-packages/eslint-changed/README.md @@ -0,0 +1,87 @@ +# ESLint Changed + +Run [ESLint] on files and only report new warnings and errors. + +## Installation + +Install via your favorite JS package manager. Note the peer dependency on eslint. + +For example, +``` +npm install eslint-changed eslint +``` + +## Usage + +To identify the changes, `eslint-changed` needs the ESLint output for both the old and new versions of the file, as well as the diff between them. +If you use git, it can determine this automatically. Otherwise, you can supply the necessary information manually. + +Options used in both modes are: + +* `--debug`: Enable debug output. +* `--ext `: Comma-separated list of JavaScript file extensions. Ignored if files are listed. (default: ".js") +* `--format `: ESLint format to use for output. (default: "stylish") +* `--in-diff-only`: Only include messages on lines changed in the diff. This may miss things like deleting a `var` that leads to a new `no-undef` elsewhere. + +### Manual diff + +The following options are used with manual mode: + +* `--diff `: A file containing the unified diff of the changes. +* `--diff-base `: Base directory the diff is relative to. Defaults to the current directory. +* `--eslint-orig `: A file containing the JSON output of eslint on the unchanged files. +* `--eslint-new `: A file containing the JSON output of eslint on the changed files. + +### With git + +In git mode, `eslint-changed` needs to be able to run `git` and `eslint`. If these are not available by those names in the shell path, +set environment variables `GIT` and/or `ESLINT` as appropriate. + +The following options are used with manual mode: + +* `--git`: Signify that you're using git mode. +* `--git-staged`: Compare the staged version to the HEAD version (this is the default). +* `--git-unstaged`: Compare the working copy version to the staged (or HEAD) version. +* `--git-base `: Compare the HEAD version to the HEAD of a different base (e.g. branch). + +## Examples + +This will compare the staged changes with HEAD. +```bash +npx eslint-changed --git +``` + +This will compare HEAD with origin/master. +```bash +npx eslint-changed --git --git-base origin/master +``` + +This does much the same as the previous example, but manually. If you're using something other than git, you might do something like this. +```bash +# Produce a diff. +git diff origin/master...HEAD > /tmp/diff + +# Check out the merge-base of origin/master and HEAD. +git checkout origin/master...HEAD + +# Run ESLint. +npx eslint --format=json . > /tmp/eslint.orig.json + +# Go back to HEAD. +git checkout - + +# Run ESLint again. +npx eslint --format=json . > /tmp/eslint.new.json + +# Run eslint-changed. +npx eslint-changed --diff /tmp/diff --eslint-orig /tmp/eslint.orig.json --eslint=new /tmp/eslint.new.json +``` +Note that, to be exactly the same as the above, you'd want to extract the list of files from the diff instead of linting everything. But this will work. + +## Inspiration + +We had been using [phpcs-changed] for a while, and wanted the same thing for ESLint. + + +[ESLint]: https://www.npmjs.com/package/eslint +[phpcs-changed]: https://packagist.org/packages/sirbrillig/phpcs-changed diff --git a/tools/eslint-changed.js b/projects/js-packages/eslint-changed/bin/eslint-changed.js similarity index 80% rename from tools/eslint-changed.js rename to projects/js-packages/eslint-changed/bin/eslint-changed.js index 50fa65218b840..99de664fceb41 100755 --- a/tools/eslint-changed.js +++ b/projects/js-packages/eslint-changed/bin/eslint-changed.js @@ -8,10 +8,12 @@ const fs = require( 'fs' ); const path = require( 'path' ); const chalk = require( 'chalk' ); +const APP_VERSION = '1.0.1-alpha'; + const { program } = require( 'commander' ); program .usage( - 'Run eslint on files and only report new warnings/errors compared to the previous version.' + 'Run ESLint on files and only report new warnings/errors compared to the previous version.' ) .option( '--diff ', 'A file containing a unified diff of the changes.' ) @@ -22,11 +24,11 @@ program ) .option( '--eslint-orig ', - 'A file containing the JSON output of eslint on the unchanged files.' + 'A file containing the JSON output of ESLint on the unchanged files.' ) .option( '--eslint-new ', - 'A file containing the JSON output of eslint on the changed files.' + 'A file containing the JSON output of ESLint on the changed files.' ) .option( @@ -46,8 +48,12 @@ program 'Comma-separated list of JavaScript file extensions. Ignored if files are listed.', '.js' ) - .option( '--format ', 'Eslint format to use for output.', 'stylish' ) - .version( '1.0.0' ); + .option( + '--in-diff-only', + 'Only include messages on lines changed in the diff. This may miss things like deleting a `var` that leads to a new `no-undef` elsewhere.' + ) + .option( '--format ', 'ESLint format to use for output.', 'stylish' ) + .version( APP_VERSION ); program.parse(); const argv = program.opts(); @@ -139,11 +145,11 @@ async function main() { ret = spawnSync( eslint, [ '--version' ], spawnOpt ); if ( ret.error ) { console.error( - `error: failed to execute eslint as \`${ eslint }\`. Use environment variable \`ESLINT\` to override.` + `error: failed to execute ESLint as \`${ eslint }\`. Use environment variable \`ESLINT\` to override.` ); process.exit( 1 ); } - debug( 'Using eslint version', ret.stdout.trim() ); + debug( 'Using ESLint version', ret.stdout.trim() ); args = [ 'rev-parse', '--show-toplevel' ]; debug( 'Getting git top level:', git, args.join( ' ' ) ); @@ -169,8 +175,13 @@ async function main() { debug( 'Running git diff command:', git, args.join( ' ' ) ); diff = parseDiff( doCmd( git, args ) ); - files = getFilesFromDiff( diff ); - debug( 'Determined files from diff:', files ); + if ( ! argv.inDiffOnly && program.args.length ) { + files = program.args; + debug( 'Determined files from command line:', files ); + } else { + files = getFilesFromDiff( diff ); + debug( 'Determined files from diff:', files ); + } eslintOrig = []; eslintNew = []; @@ -180,13 +191,13 @@ async function main() { args = [ 'cat-file', '-e', origRef + ':' + file ]; debug( 'Testing if file is new:', git, args.join( ' ' ) ); if ( spawnSync( git, args, { stdio: 'ignore' } ).status ) { - debug( "It's new, so no orig eslint data." ); + debug( "It's new, so no orig ESLint data." ); } else { args = [ 'show', origRef + ':' + file ]; debug( 'Fetching orig file contents:', git, args.join( ' ' ) ); content = doCmd( git, args ); args = eslintArgs.concat( [ '--stdin', '--stdin-filename', file, '--format=json' ] ); - debug( 'Executing eslint for orig file:', eslint, args.join( ' ' ) ); + debug( 'Executing ESLint for orig file:', eslint, args.join( ' ' ) ); ret = spawnSync( eslint, args, { ...spawnOpt, input: content } ); if ( ret.error ) { throw ret.error; @@ -202,7 +213,7 @@ async function main() { content = doCmd( git, args ); } args = eslintArgs.concat( [ '--stdin', '--stdin-filename', file, '--format=json' ] ); - debug( 'Executing eslint for new file:', eslint, args.join( ' ' ) ); + debug( 'Executing ESLint for new file:', eslint, args.join( ' ' ) ); ret = spawnSync( eslint, args, { ...spawnOpt, input: content } ); if ( ret.error ) { throw ret.error; @@ -212,8 +223,18 @@ async function main() { } else if ( argv.diff ) { diff = parseDiff( fs.readFileSync( argv.diff, 'utf8' ) ); diffBase = argv.diffBase || process.cwd(); - files = getFilesFromDiff( diff ); - debug( 'Determined files from diff:', files ); + if ( ! argv.inDiffOnly && program.args.length ) { + files = program.args; + debug( 'Determined files from command line:', files ); + } else { + files = getFilesFromDiff( diff ); + debug( 'Determined files from diff:', files ); + if ( program.args.length ) { + const cmdLineFiles = new Set( program.args ); + files = files.filter( file => cmdLineFiles.has( file ) ); + debug( 'Intersected files with those from the command line:', files ); + } + } eslintOrig = JSON.parse( fs.readFileSync( argv.eslintOrig, 'utf8' ) ); eslintNew = JSON.parse( fs.readFileSync( argv.eslintNew, 'utf8' ) ); } else { @@ -221,6 +242,8 @@ async function main() { process.exit( 1 ); } + // oldLines maps line numbers in the old version to the new. + // newLines just lists lines present in the diff. const oldLines = {}; const newLines = {}; diff.forEach( file => { @@ -250,22 +273,25 @@ async function main() { newLines[ fileName ] = nl; } ); - eslintOrig = eslintOrig.filter( x => oldLines[ x.filePath ] ); - eslintNew = eslintNew.filter( x => newLines[ x.filePath ] ); + if ( argv.inDiffOnly ) { + files = new Set( files.map( file => path.resolve( diffBase, file ) ) ); + eslintOrig = eslintOrig.filter( x => files.has( x.filePath ) && oldLines[ x.filePath ] ); + eslintNew = eslintNew.filter( x => files.has( x.filePath ) && newLines[ x.filePath ] ); + } const origMsgs = {}; eslintOrig.forEach( file => { const lines = {}; const oldL = oldLines[ file.filePath ] || {}; file.messages.forEach( msg => { - if ( ! oldL[ msg.line ] ) { - debug( - `Orig ${ file.filePath }: Ignoring ${ msg.ruleId } on line ${ msg.line }, not in diff` - ); - return; + let line = msg.line; + for ( let i = msg.line; i > 0; i-- ) { + if ( oldL[ i ] ) { + line = msg.line + oldL[ i ] - i; + break; + } } - const line = oldL[ msg.line ]; debug( `Orig ${ file.filePath }: Found ${ msg.ruleId } on line ${ msg.line } => ${ line }` ); if ( ! lines[ line ] ) { lines[ line ] = {}; @@ -287,7 +313,7 @@ async function main() { file.fixableWarningCount = 0; messages.forEach( msg => { - if ( ! newL[ msg.line ] ) { + if ( argv.inDiffOnly && ! newL[ msg.line ] ) { debug( `New ${ file.filePath }: Ignoring ${ msg.ruleId } on line ${ msg.line }, not in diff` ); diff --git a/projects/js-packages/eslint-changed/changelog/.gitkeep b/projects/js-packages/eslint-changed/changelog/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/projects/js-packages/eslint-changed/changelog/update-move-eslint-changed-to-project b/projects/js-packages/eslint-changed/changelog/update-move-eslint-changed-to-project new file mode 100644 index 0000000000000..7177993bf6d24 --- /dev/null +++ b/projects/js-packages/eslint-changed/changelog/update-move-eslint-changed-to-project @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Initial release as a project. Added tests. diff --git a/projects/js-packages/eslint-changed/composer.json b/projects/js-packages/eslint-changed/composer.json new file mode 100644 index 0000000000000..b898da3a9aa94 --- /dev/null +++ b/projects/js-packages/eslint-changed/composer.json @@ -0,0 +1,33 @@ +{ + "name": "automattic/eslint-changed", + "description": "description", + "license": "GPL-2.0-or-later", + "require": {}, + "require-dev": { + "automattic/jetpack-changelogger": "^1.2" + }, + "scripts": { + "test-coverage": [ + "pnpx nyc --report-dir=\"$COVERAGE_DIR\" pnpm run test" + ], + "test-js": [ + "pnpm run test" + ] + }, + "repositories": [ + { + "type": "path", + "url": "../../packages/*", + "options": { + "monorepo": true + } + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "extra": { + "version-constants": { + "::APP_VERSION": "bin/eslint-changed.js" + } + } +} diff --git a/projects/js-packages/eslint-changed/license.txt b/projects/js-packages/eslint-changed/license.txt new file mode 100644 index 0000000000000..e82774c1bd5d4 --- /dev/null +++ b/projects/js-packages/eslint-changed/license.txt @@ -0,0 +1,357 @@ +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +=================================== + + +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + +We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + +Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and +modification follow. + +GNU GENERAL PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + +a) You must cause the modified files to carry prominent notices +stating that you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in +whole or in part contains or is derived from the Program or any +part thereof, to be licensed as a whole at no charge to all third +parties under the terms of this License. + +c) If the modified program normally reads commands interactively +when run, you must cause it, when started running for such +interactive use in the most ordinary way, to print or display an +announcement including an appropriate copyright notice and a +notice that there is no warranty (or else, saying that you provide +a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this +License. (Exception: if the Program itself is interactive but +does not normally print such an announcement, your work based on +the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable +source code, which must be distributed under the terms of Sections +1 and 2 above on a medium customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three +years, to give any third party, for a charge no more than your +cost of physically performing source distribution, a complete +machine-readable copy of the corresponding source code, to be +distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer +to distribute corresponding source code. (This alternative is +allowed only for noncommercial distribution and only if you +received the program in object code or executable form with such +an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + +5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + +10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + +Copyright (C) + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author +Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program +`Gnomovision' (which makes passes at compilers) written by James Hacker. + +, 1 April 1989 +Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/projects/js-packages/eslint-changed/package.json b/projects/js-packages/eslint-changed/package.json new file mode 100644 index 0000000000000..d774cc67c3d62 --- /dev/null +++ b/projects/js-packages/eslint-changed/package.json @@ -0,0 +1,39 @@ +{ + "name": "@automattic/eslint-changed", + "version": "1.0.1-alpha", + "description": "Run eslint on files, but only report warnings and errors from lines that were changed.", + "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/eslint-changed/README.md", + "bugs": { + "url": "https://github.com/Automattic/jetpack/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/Automattic/jetpack.git" + }, + "license": "GPL-2.0-or-later", + "author": "Automattic", + "bin": { + "eslint-changed": "bin/eslint-changed.js" + }, + "scripts": { + "test": "js-test-runner 'glob:tests/**/*.js'" + }, + "dependencies": { + "chalk": "4.1.1", + "commander": "7.2.0", + "parse-diff": "0.8.1" + }, + "devDependencies": { + "eslint": "7.25.0", + "jetpack-js-test-runner": "workspace:*", + "nyc": "15.1.0" + }, + "peerDependencies": { + "eslint": ">=1.1.0" + }, + "engines": { + "node": "^14.16.0", + "pnpm": "^6.5.0", + "yarn": "use pnpm instead - see docs/yarn-upgrade.md" + } +} diff --git a/projects/js-packages/eslint-changed/tests/bin/eslint-changed.js b/projects/js-packages/eslint-changed/tests/bin/eslint-changed.js new file mode 100644 index 0000000000000..691d3089c13c0 --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/bin/eslint-changed.js @@ -0,0 +1,792 @@ +const assert = require( 'chai' ).assert; +const childProcess = require( 'child_process' ); +const ChildProcess = childProcess.ChildProcess; +const fs = require( 'fs/promises' ); +const os = require( 'os' ); +const path = require( 'path' ); + +/** + * Wait for a process to exit. + * + * @param {ChildProcess} proc - The child process. + * @returns {Promise} A promise that resolves with an object holding the exit code, stdout, and stderr. + */ +function awaitExit( proc ) { + let stdout = ''; + let stderr = ''; + + proc.stdout.on( 'data', data => ( stdout += data ) ); + proc.stderr.on( 'data', data => ( stderr += data ) ); + return new Promise( resolve => + proc.once( 'exit', exitCode => resolve( { exitCode, stdout, stderr } ) ) + ); +} + +describe( 'bin/eslint-changed.js', () => { + const processes = new Set(); + let tmpdir = null; + + /** + * Run eslint-changed. + * + * @param {string[]} [args] - Arguments to pass. + * @param {object} [options] - Options for child_process. + * @returns {ChildProcess} The child process. + */ + function runEslintChanged( args, options ) { + const proc = childProcess.fork( path.join( __dirname, '../../bin/eslint-changed.js' ), args, { + silent: true, + ...options, + } ); + processes.add( proc ); + return proc; + } + + /** + * Set up a temporary directory. + * + * The path is stored in `tmpdir`. + */ + async function mktmpdir() { + if ( tmpdir ) { + await fs.rm( tmpdir, { force: true, recursive: true } ); + tmpdir = null; + } + tmpdir = await fs.mkdtemp( path.join( os.tmpdir(), 'eslint-changed-test-' ) ); + } + + // Clean up after each test. + afterEach( async () => { + // Clean up the temporary directory, if any. + if ( tmpdir ) { + await fs.rm( tmpdir, { force: true, recursive: true } ); + tmpdir = null; + } + + // Clean up any leftover processes. + processes.forEach( proc => proc.kill() ); + processes.clear(); + } ); + + it( 'Accepts --version', async () => { + const proc = await runEslintChanged( [ '--version' ] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 0, 'Exit code is 0' ); + assert.match( + data.stdout, + /^\d+\.\d+\.\d+(?:-alpha)?\n$/s, + 'Output looks like a version number' + ); + } ); + + it( 'Fails when not passed --diff or --git', async () => { + const proc = await runEslintChanged( [] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + } ); + + it( 'Fails when passed both --diff and --git', async () => { + const proc = await runEslintChanged( [ '--git', '--diff', 'foo' ] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + } ); + + describe( 'Manual mode', () => { + it( 'Works in manual mode', async () => { + const proc = await runEslintChanged( [ + '--format=json', + '--diff', + path.join( __dirname, '../fixtures/manual-mode.diff' ), + '--diff-base', + '/tmp/x/t', + '--eslint-orig', + path.join( __dirname, '../fixtures/manual-mode.orig.json' ), + '--eslint-new', + path.join( __dirname, '../fixtures/manual-mode.new.json' ), + ] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = JSON.parse( + await fs.readFile( path.join( __dirname, '../fixtures/manual-mode.expect.json' ) ) + ); + assert.deepEqual( output, expect ); + } ); + + it( 'Exit 0 when no new errors', async () => { + const proc = await runEslintChanged( [ + '--format=json', + '--diff', + path.join( __dirname, '../fixtures/no-new-errors.diff' ), + '--diff-base', + '/tmp/x/t', + '--eslint-orig', + path.join( __dirname, '../fixtures/no-new-errors.orig.json' ), + '--eslint-new', + path.join( __dirname, '../fixtures/no-new-errors.new.json' ), + ] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 0, 'Exit code is 0' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = JSON.parse( + await fs.readFile( path.join( __dirname, '../fixtures/no-new-errors.expect.json' ) ) + ); + assert.deepEqual( output, expect ); + } ); + + it( 'Filters by filename', async () => { + const proc = await runEslintChanged( [ + '--format=json', + '--diff', + path.join( __dirname, '../fixtures/files-123.diff' ), + '--diff-base', + '/tmp/x/t', + '--eslint-orig', + path.join( __dirname, '../fixtures/files-123.orig.json' ), + '--eslint-new', + path.join( __dirname, '../fixtures/files-123.new.json' ), + '1.js', + '3.js', + ] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = JSON.parse( + await fs.readFile( path.join( __dirname, '../fixtures/files-123.expect.json' ) ) + ); + assert.deepEqual( output, expect ); + } ); + + it( 'Includes new errors in unchanged lines', async () => { + const proc = await runEslintChanged( [ + '--format=json', + '--diff', + path.join( __dirname, '../fixtures/new-err-not-in-diff.diff' ), + '--diff-base', + '/tmp/x/t', + '--eslint-orig', + path.join( __dirname, '../fixtures/new-err-not-in-diff.orig.json' ), + '--eslint-new', + path.join( __dirname, '../fixtures/new-err-not-in-diff.new.json' ), + ] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = JSON.parse( + await fs.readFile( path.join( __dirname, '../fixtures/new-err-not-in-diff.expect.json' ) ) + ); + assert.deepEqual( output, expect ); + } ); + + it( 'Does not include new errors in unchanged lines with --in-diff-only', async () => { + const proc = await runEslintChanged( [ + '--format=json', + '--in-diff-only', + '--diff', + path.join( __dirname, '../fixtures/new-err-not-in-diff.diff' ), + '--diff-base', + '/tmp/x/t', + '--eslint-orig', + path.join( __dirname, '../fixtures/new-err-not-in-diff.orig.json' ), + '--eslint-new', + path.join( __dirname, '../fixtures/new-err-not-in-diff.new.json' ), + ] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 0, 'Exit code is 0' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = JSON.parse( + await fs.readFile( path.join( __dirname, '../fixtures/new-err-not-in-diff.expect2.json' ) ) + ); + assert.deepEqual( output, expect ); + } ); + + it( 'Gets diff-base from cwd', async () => { + await mktmpdir(); + + for ( const suffix of [ 'diff', 'orig.json', 'new.json', 'expect.json' ] ) { + const data = await fs.readFile( + path.join( __dirname, `../fixtures/manual-mode.${ suffix }` ), + 'utf-8' + ); + await fs.writeFile( + path.join( tmpdir, `test.${ suffix }` ), + data.replace( /\/tmp\/x\/t\//g, tmpdir + '/' ) + ); + } + + const proc = await runEslintChanged( + [ + '--format=json', + '--diff', + 'test.diff', + '--eslint-orig', + 'test.orig.json', + '--eslint-new', + 'test.new.json', + ], + { cwd: tmpdir } + ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = JSON.parse( await fs.readFile( path.join( tmpdir, 'test.expect.json' ) ) ); + assert.deepEqual( output, expect ); + } ); + + const argsets = [ + [ '--diff', path.join( __dirname, '../fixtures/no-new-errors.diff' ) ], + [ '--eslint-orig', path.join( __dirname, '../fixtures/no-new-errors.orig.json' ) ], + [ '--eslint-new', path.join( __dirname, '../fixtures/no-new-errors.new.json' ) ], + ]; + for ( let i = ( 1 << argsets.length ) - 2; i > 0; i-- ) { + const args = []; + const missing = []; + for ( let j = 0; j < argsets.length; j++ ) { + if ( i & ( 1 << j ) ) { + args.push( ...argsets[ j ] ); + } else { + missing.push( argsets[ j ][ 0 ] ); + } + } + + it( `Fails with incomplete arguments: missing ${ missing.join( ' ' ) }`, async () => { + const proc = await runEslintChanged( [ '--format=json', ...args ] ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + } ); + } + } ); + + describe( 'Git mode', function () { + this.timeout( 5000 ); + + /** + * Set up a temporary directory with a git repo. + * + * The path is stored in `tmpdir`. + * + * @param {object[]} branches - An array of branches to create. + * @param {string} [branches.name] - Name of the branch. + * @param {string} [branches.parent] - Name of the parent branch. If omitted, the parent is the previous entry in the array. Must be omitted in the first entry. + * @param {object} branches.files - Files to modify, and their contents (or null to delete the file). + * @param {object} [staged] - Files to modify and stage. + * @param {object} [unstaged] - Files to modify and leave unstaged. + */ + async function mktmpdirgit( branches, staged, unstaged ) { + await mktmpdir(); + + const opts = { + cwd: tmpdir, + env: { + GIT_AUTHOR_NAME: 'Testing', + GIT_AUTHOR_EMAIL: 'nobody@example.com', + GIT_COMMITTER_NAME: 'Testing', + GIT_COMMITTER_EMAIL: 'nobody@example.com', + }, + }; + childProcess.spawnSync( 'git', [ 'init', '.' ], opts ); + + /** + * Modify files. + * + * @param {object} files - Files to modify, and their contents (or null to delete the file). + * @param {boolean} git - Whether to do git manipulations. + */ + async function doFiles( files, git ) { + const modified = []; + const removed = []; + + for ( const [ fileName, contents ] of Object.entries( files ) ) { + const filePath = path.join( tmpdir, fileName ); + + if ( contents === null ) { + await fs.rm( filePath, { force: true, recursive: true } ); + removed.push( fileName ); + } else { + await fs.mkdir( path.dirname( filePath ), { recursive: true } ); + await fs.writeFile( filePath, contents ); + modified.push( fileName ); + } + } + + if ( git && removed.length ) { + childProcess.spawnSync( 'git', [ 'rm', '-f', ...removed ], opts ); + } + if ( git && modified.length ) { + childProcess.spawnSync( 'git', [ 'add', '-f', ...modified ], opts ); + } + } + + for ( const b of branches ) { + if ( b.parent ) { + childProcess.spawnSync( 'git', [ 'checkout', b.parent ], opts ); + } + if ( b.name ) { + childProcess.spawnSync( 'git', [ 'checkout', '-B', b.name ], opts ); + } else { + childProcess.spawnSync( 'git', [ 'checkout', '--detach' ], opts ); + } + await doFiles( b.files, true ); + childProcess.spawnSync( 'git', [ 'commit', '-m', 'Testing' ], opts ); + } + + if ( staged ) { + await doFiles( staged, true ); + } + if ( unstaged ) { + await doFiles( unstaged, false ); + } + } + + const eslintrc = JSON.stringify( + { + extends: 'eslint:recommended', + env: { + node: true, + }, + rules: { + indent: [ 2, 'tab' ], + quotes: [ 2, 'single' ], + 'linebreak-style': [ 2, 'unix' ], + semi: [ 2, 'always' ], + }, + }, + null, + 4 + ); + + const standardRepo = [ + [ + { + name: 'base', + files: { + '.eslintrc': eslintrc, + '1.js': "console.log( 'Hello, world!' );\n", + '2.js': "console.log( 'Hello, world!' );\n", + '3.js': "console.log( 'Hello, world!' );\n", + }, + }, + { + name: 'master', + files: { + '1.js': "var x;\nconsole.log( 'Hello, world!' );\n", + }, + }, + ], + { + '2.js': 'console.log( "Hello, world?" );\n', + }, + { + '3.js': "console.log( '¡Hola, mundo!' )\n", + }, + ]; + + it( 'Fails gracefully without git', async () => { + const proc = await runEslintChanged( [ '--format=json', '--git' ], { + cwd: tmpdir, + env: { GIT: 'this-command-really-should-not-exist' }, + } ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + assert.strictEqual( + data.stderr, + 'error: failed to execute git as `this-command-really-should-not-exist`. Use environment variable `GIT` to override.\n' + ); + } ); + + it( 'Fails gracefully without ESLint', async () => { + const proc = await runEslintChanged( [ '--format=json', '--git' ], { + cwd: tmpdir, + env: { GIT: 'true', ESLINT: 'this-command-really-should-not-exist-either' }, + } ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + assert.strictEqual( + data.stderr, + 'error: failed to execute ESLint as `this-command-really-should-not-exist-either`. Use environment variable `ESLINT` to override.\n' + ); + } ); + + it( 'Works in git mode, --git-staged is the default', async () => { + await mktmpdirgit( ...standardRepo ); + + const proc = await runEslintChanged( [ '--format=json', '--git' ], { cwd: tmpdir } ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = [ + { + filePath: path.join( tmpdir, '2.js' ), + messages: [ + { + ruleId: 'quotes', + severity: 2, + message: 'Strings must use singlequote.', + line: 1, + column: 14, + nodeType: 'Literal', + messageId: 'wrongQuotes', + endLine: 1, + endColumn: 29, + fix: { + range: [ 13, 28 ], + text: "'Hello, world?'", + }, + }, + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 1, + fixableWarningCount: 0, + source: 'console.log( "Hello, world?" );\n', + usedDeprecatedRules: [], + }, + ]; + assert.deepEqual( output, expect ); + } ); + + it( 'Works in git mode, explicit --git-staged', async () => { + await mktmpdirgit( ...standardRepo ); + + const proc = await runEslintChanged( [ '--format=json', '--git', '--git-staged' ], { + cwd: tmpdir, + } ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = [ + { + filePath: path.join( tmpdir, '2.js' ), + messages: [ + { + ruleId: 'quotes', + severity: 2, + message: 'Strings must use singlequote.', + line: 1, + column: 14, + nodeType: 'Literal', + messageId: 'wrongQuotes', + endLine: 1, + endColumn: 29, + fix: { + range: [ 13, 28 ], + text: "'Hello, world?'", + }, + }, + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 1, + fixableWarningCount: 0, + source: 'console.log( "Hello, world?" );\n', + usedDeprecatedRules: [], + }, + ]; + assert.deepEqual( output, expect ); + } ); + + it( 'Works with --git-unstaged', async () => { + await mktmpdirgit( ...standardRepo ); + + const proc = await runEslintChanged( [ '--format=json', '--git', '--git-unstaged' ], { + cwd: tmpdir, + } ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = [ + { + filePath: path.join( tmpdir, '3.js' ), + messages: [ + { + ruleId: 'semi', + severity: 2, + message: 'Missing semicolon.', + line: 1, + column: 31, + nodeType: 'ExpressionStatement', + messageId: 'missingSemi', + endLine: 2, + endColumn: 1, + fix: { + range: [ 30, 30 ], + text: ';', + }, + }, + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 1, + fixableWarningCount: 0, + source: "console.log( '¡Hola, mundo!' )\n", + usedDeprecatedRules: [], + }, + ]; + assert.deepEqual( output, expect ); + } ); + + it( 'Works with --git-base', async () => { + await mktmpdirgit( ...standardRepo ); + + const proc = await runEslintChanged( [ '--format=json', '--git', '--git-base', 'base' ], { + cwd: tmpdir, + } ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = [ + { + filePath: path.join( tmpdir, '1.js' ), + messages: [ + { + ruleId: 'no-unused-vars', + severity: 2, + message: "'x' is defined but never used.", + line: 1, + column: 5, + nodeType: 'Identifier', + messageId: 'unusedVar', + endLine: 1, + endColumn: 6, + }, + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var x;\nconsole.log( 'Hello, world!' );\n", + usedDeprecatedRules: [], + }, + ]; + assert.deepEqual( output, expect ); + } ); + + it( 'Works with added and deleted files', async () => { + await mktmpdirgit( + [ + { + files: { + '.eslintrc': eslintrc, + 'unchanged.js': "console.log( 'Hello, world!' )\n", + 'modified.js': "var x = 'Hello';\nx += ', world!';\nconsole.log( x );\n", + 'deleted.js': "console.log( 'Hello, world!' )\n", + }, + }, + ], + { + 'modified.js': 'var x = \'Hello\';\nx += ", world!";\nconsole.log( x );\n', + 'deleted.js': null, + 'added.js': 'var x = 1;\n', + } + ); + + const proc = await runEslintChanged( [ '--format=json', '--git' ], { cwd: tmpdir } ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = [ + { + filePath: path.join( tmpdir, 'added.js' ), + messages: [ + { + ruleId: 'no-unused-vars', + severity: 2, + message: "'x' is assigned a value but never used.", + line: 1, + column: 5, + nodeType: 'Identifier', + messageId: 'unusedVar', + endLine: 1, + endColumn: 6, + }, + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: 'var x = 1;\n', + usedDeprecatedRules: [], + }, + { + filePath: path.join( tmpdir, 'modified.js' ), + messages: [ + { + ruleId: 'quotes', + severity: 2, + message: 'Strings must use singlequote.', + line: 2, + column: 6, + nodeType: 'Literal', + messageId: 'wrongQuotes', + endLine: 2, + endColumn: 16, + fix: { + range: [ 22, 32 ], + text: "', world!'", + }, + }, + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 1, + fixableWarningCount: 0, + source: 'var x = \'Hello\';\nx += ", world!";\nconsole.log( x );\n', + usedDeprecatedRules: [], + }, + ]; + assert.deepEqual( output, expect ); + } ); + + it( 'Works with explicitly specified files', async () => { + await mktmpdirgit( + [ + { + files: { + '.eslintrc': eslintrc, + '1.js': "var x = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + '2.js': "var x = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + '3.js': "var x = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + }, + }, + ], + { + '2.js': "var y = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + '3.js': "var y = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + } + ); + + const proc = await runEslintChanged( [ '--format=json', '--git', '1.js', '2.js' ], { + cwd: tmpdir, + } ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = [ + { + filePath: path.join( tmpdir, '1.js' ), + messages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var x = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + usedDeprecatedRules: [], + }, + { + filePath: path.join( tmpdir, '2.js' ), + messages: [ + { + ruleId: 'no-unused-vars', + severity: 2, + message: "'y' is assigned a value but never used.", + line: 1, + column: 5, + nodeType: 'Identifier', + messageId: 'unusedVar', + endLine: 1, + endColumn: 6, + }, + { + ruleId: 'no-undef', + severity: 2, + message: "'x' is not defined.", + line: 13, + column: 14, + nodeType: 'Identifier', + messageId: 'undef', + endLine: 13, + endColumn: 15, + }, + ], + errorCount: 2, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var y = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + usedDeprecatedRules: [], + }, + ]; + assert.deepEqual( output, expect ); + } ); + + it( 'Works with --in-diff-only', async () => { + await mktmpdirgit( + [ + { + files: { + '.eslintrc': eslintrc, + '1.js': "var x = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + '2.js': "var x = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + '3.js': "var x = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + }, + }, + ], + { + '2.js': "var y = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + '3.js': "var y = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + } + ); + + const proc = await runEslintChanged( + [ '--format=json', '--git', '--in-diff-only', '1.js', '2.js' ], + { + cwd: tmpdir, + } + ); + const data = await awaitExit( proc ); + assert.strictEqual( data.exitCode, 1, 'Exit code is 1' ); + + const output = JSON.parse( data.stdout ); + assert.isArray( output, 'Output is a JSON array' ); + const expect = [ + { + filePath: path.join( tmpdir, '2.js' ), + messages: [ + { + ruleId: 'no-unused-vars', + severity: 2, + message: "'y' is assigned a value but never used.", + line: 1, + column: 5, + nodeType: 'Identifier', + messageId: 'unusedVar', + endLine: 1, + endColumn: 6, + }, + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var y = 'Hello, world!';\n\n\n\n\n\n\n\n\n\n\n\nconsole.log( x )\n", + usedDeprecatedRules: [], + }, + ]; + assert.deepEqual( output, expect ); + } ); + } ); +} ); diff --git a/projects/js-packages/eslint-changed/tests/fixtures/files-123.diff b/projects/js-packages/eslint-changed/tests/fixtures/files-123.diff new file mode 100644 index 0000000000000..97c22d22d9b3a --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/files-123.diff @@ -0,0 +1,15 @@ +diff -urN a/1.js b/1.js +--- a/1.js 2021-06-16 14:34:57.077022517 -0400 ++++ b/1.js 2021-06-16 14:34:07.504805277 -0400 +@@ -0,0 +1 @@ ++var x; +diff -urN a/2.js b/2.js +--- a/2.js 2021-06-16 14:34:58.993030912 -0400 ++++ b/2.js 2021-06-16 14:34:09.780815251 -0400 +@@ -0,0 +1 @@ ++var x; +diff -urN a/3.js b/3.js +--- a/3.js 2021-06-16 14:35:00.181036119 -0400 ++++ b/3.js 2021-06-16 14:34:11.424822454 -0400 +@@ -0,0 +1 @@ ++var x; diff --git a/projects/js-packages/eslint-changed/tests/fixtures/files-123.expect.json b/projects/js-packages/eslint-changed/tests/fixtures/files-123.expect.json new file mode 100644 index 0000000000000..cc7a7c979de6e --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/files-123.expect.json @@ -0,0 +1,68 @@ +[ + { + "filePath": "/tmp/x/t/1.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'x' is defined but never used.", + "line": 1, + "column": 5, + "nodeType": "Identifier", + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 6 + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "var x;\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/2.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'x' is defined but never used.", + "line": 1, + "column": 5, + "nodeType": "Identifier", + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 6 + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "var x;\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/3.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'x' is defined but never used.", + "line": 1, + "column": 5, + "nodeType": "Identifier", + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 6 + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "var x;\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/files-123.new.json b/projects/js-packages/eslint-changed/tests/fixtures/files-123.new.json new file mode 100644 index 0000000000000..cc7a7c979de6e --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/files-123.new.json @@ -0,0 +1,68 @@ +[ + { + "filePath": "/tmp/x/t/1.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'x' is defined but never used.", + "line": 1, + "column": 5, + "nodeType": "Identifier", + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 6 + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "var x;\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/2.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'x' is defined but never used.", + "line": 1, + "column": 5, + "nodeType": "Identifier", + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 6 + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "var x;\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/3.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'x' is defined but never used.", + "line": 1, + "column": 5, + "nodeType": "Identifier", + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 6 + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "var x;\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/files-123.orig.json b/projects/js-packages/eslint-changed/tests/fixtures/files-123.orig.json new file mode 100644 index 0000000000000..b96713e7084d7 --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/files-123.orig.json @@ -0,0 +1,29 @@ +[ + { + "filePath": "/tmp/x/t/1.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/2.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/3.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.diff b/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.diff new file mode 100644 index 0000000000000..c9b7da54c62a7 --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.diff @@ -0,0 +1,9 @@ +diff -urN a/foo.js b/foo.js +--- a/foo.js 2021-06-16 14:00:16.244119980 -0400 ++++ b/foo.js 2021-06-16 14:02:24.600650825 -0400 +@@ -1,4 +1,5 @@ + var x = 123; + + console.log( "This is a thing" ) ++console.log( "Really!" ) + console.log( "x = ", x ) diff --git a/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.expect.json b/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.expect.json new file mode 100644 index 0000000000000..2cdbf73750153 --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.expect.json @@ -0,0 +1,34 @@ +[ + { + "filePath": "/tmp/x/t/bar.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/foo.js", + "messages": [ + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 4, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 4, + "endColumn": 23, + "fix": { "range": [ 61, 70 ], "text": "'Really!'" } + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 1, + "fixableWarningCount": 0, + "source": "var x = 123;\n\nconsole.log( \"This is a thing\" );\nconsole.log( \"Really!\" );\nconsole.log( \"x = \", x );\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.new.json b/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.new.json new file mode 100644 index 0000000000000..2e46dea67c39d --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.new.json @@ -0,0 +1,58 @@ +[ + { + "filePath": "/tmp/x/t/bar.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/foo.js", + "messages": [ + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 3, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 3, + "endColumn": 31, + "fix": { "range": [ 27, 44 ], "text": "'This is a thing'" } + }, + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 4, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 4, + "endColumn": 23, + "fix": { "range": [ 61, 70 ], "text": "'Really!'" } + }, + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 5, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 5, + "endColumn": 20, + "fix": { "range": [ 87, 93 ], "text": "'x = '" } + } + ], + "errorCount": 3, + "warningCount": 0, + "fixableErrorCount": 3, + "fixableWarningCount": 0, + "source": "var x = 123;\n\nconsole.log( \"This is a thing\" );\nconsole.log( \"Really!\" );\nconsole.log( \"x = \", x );\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.orig.json b/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.orig.json new file mode 100644 index 0000000000000..0cd53b8869767 --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/manual-mode.orig.json @@ -0,0 +1,70 @@ +[ + { + "filePath": "/tmp/x/t/bar.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/foo.js", + "messages": [ + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 3, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 3, + "endColumn": 31, + "fix": { "range": [ 27, 44 ], "text": "'This is a thing'" } + }, + { + "ruleId": "semi", + "severity": 2, + "message": "Missing semicolon.", + "line": 3, + "column": 33, + "nodeType": "ExpressionStatement", + "messageId": "missingSemi", + "endLine": 4, + "endColumn": 1, + "fix": { "range": [ 46, 46 ], "text": ";" } + }, + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 4, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 4, + "endColumn": 20, + "fix": { "range": [ 60, 66 ], "text": "'x = '" } + }, + { + "ruleId": "semi", + "severity": 2, + "message": "Missing semicolon.", + "line": 4, + "column": 25, + "nodeType": "ExpressionStatement", + "messageId": "missingSemi", + "endLine": 5, + "endColumn": 1, + "fix": { "range": [ 71, 71 ], "text": ";" } + } + ], + "errorCount": 4, + "warningCount": 0, + "fixableErrorCount": 4, + "fixableWarningCount": 0, + "source": "var x = 123;\n\nconsole.log( \"This is a thing\" )\nconsole.log( \"x = \", x )\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.diff b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.diff new file mode 100644 index 0000000000000..9afd4789292ab --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.diff @@ -0,0 +1,9 @@ +diff -ur a/foo.js b/foo.js +--- a/foo.js 2021-06-22 10:37:52.018725113 -0400 ++++ b/foo.js 2021-06-22 10:39:34.463026250 -0400 +@@ -1,5 +1,3 @@ +-var x = 123; +- + // ... + // ... + // ... diff --git a/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.expect.json b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.expect.json new file mode 100644 index 0000000000000..03427140b701e --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.expect.json @@ -0,0 +1,24 @@ +[ + { + "filePath": "/tmp/x/t/foo.js", + "messages": [ + { + "ruleId": "no-undef", + "severity": 2, + "message": "'x' is not defined.", + "line": 23, + "column": 14, + "nodeType": "Identifier", + "messageId": "undef", + "endLine": 23, + "endColumn": 15 + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n\nconsole.log( x )\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.expect2.json b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.expect2.json new file mode 100644 index 0000000000000..c3539d2b8cd86 --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.expect2.json @@ -0,0 +1,12 @@ +[ + { + "filePath": "/tmp/x/t/foo.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n\nconsole.log( x )\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.new.json b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.new.json new file mode 100644 index 0000000000000..4ff8fa26b17ce --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.new.json @@ -0,0 +1,36 @@ +[ + { + "filePath": "/tmp/x/t/foo.js", + "messages": [ + { + "ruleId": "no-undef", + "severity": 2, + "message": "'x' is not defined.", + "line": 23, + "column": 14, + "nodeType": "Identifier", + "messageId": "undef", + "endLine": 23, + "endColumn": 15 + }, + { + "ruleId": "semi", + "severity": 2, + "message": "Missing semicolon.", + "line": 23, + "column": 17, + "nodeType": "ExpressionStatement", + "messageId": "missingSemi", + "endLine": 24, + "endColumn": 1, + "fix": { "range": [ 164, 164 ], "text": ";" } + } + ], + "errorCount": 2, + "warningCount": 0, + "fixableErrorCount": 1, + "fixableWarningCount": 0, + "source": "// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n\nconsole.log( x )\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.orig.json b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.orig.json new file mode 100644 index 0000000000000..11250cb4b3876 --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/new-err-not-in-diff.orig.json @@ -0,0 +1,25 @@ +[ + { + "filePath": "/tmp/x/t/foo.js", + "messages": [ + { + "ruleId": "semi", + "severity": 2, + "message": "Missing semicolon.", + "line": 25, + "column": 17, + "nodeType": "ExpressionStatement", + "messageId": "missingSemi", + "endLine": 26, + "endColumn": 1, + "fix": { "range": [ 178, 178 ], "text": ";" } + } + ], + "errorCount": 1, + "warningCount": 0, + "fixableErrorCount": 1, + "fixableWarningCount": 0, + "source": "var x = 123;\n\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n// ...\n\nconsole.log( x )\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.diff b/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.diff new file mode 100644 index 0000000000000..17bf8b39a774d --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.diff @@ -0,0 +1,10 @@ +diff -urN a/foo.js b/foo.js +--- a/foo.js 2021-06-16 14:00:16.244119980 -0400 ++++ b/foo.js 2021-06-16 14:16:57.408409477 -0400 +@@ -1,4 +1,4 @@ + var x = 123; + +-console.log( "This is a thing" ) +-console.log( "x = ", x ) ++console.log( "This is a thing" ); ++console.log( "x = ", x ); diff --git a/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.expect.json b/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.expect.json new file mode 100644 index 0000000000000..6d7d3173f650b --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.expect.json @@ -0,0 +1,21 @@ +[ + { + "filePath": "/tmp/x/t/bar.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/foo.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "var x = 123;\n\nconsole.log( \"This is a thing\" );\nconsole.log( \"x = \", x );\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.new.json b/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.new.json new file mode 100644 index 0000000000000..b5701742767bf --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.new.json @@ -0,0 +1,46 @@ +[ + { + "filePath": "/tmp/x/t/bar.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/foo.js", + "messages": [ + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 3, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 3, + "endColumn": 31, + "fix": { "range": [ 27, 44 ], "text": "'This is a thing'" } + }, + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 4, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 4, + "endColumn": 20, + "fix": { "range": [ 61, 67 ], "text": "'x = '" } + } + ], + "errorCount": 2, + "warningCount": 0, + "fixableErrorCount": 2, + "fixableWarningCount": 0, + "source": "var x = 123;\n\nconsole.log( \"This is a thing\" );\nconsole.log( \"x = \", x );\n", + "usedDeprecatedRules": [] + } +] diff --git a/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.orig.json b/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.orig.json new file mode 100644 index 0000000000000..0cd53b8869767 --- /dev/null +++ b/projects/js-packages/eslint-changed/tests/fixtures/no-new-errors.orig.json @@ -0,0 +1,70 @@ +[ + { + "filePath": "/tmp/x/t/bar.js", + "messages": [], + "errorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "/tmp/x/t/foo.js", + "messages": [ + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 3, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 3, + "endColumn": 31, + "fix": { "range": [ 27, 44 ], "text": "'This is a thing'" } + }, + { + "ruleId": "semi", + "severity": 2, + "message": "Missing semicolon.", + "line": 3, + "column": 33, + "nodeType": "ExpressionStatement", + "messageId": "missingSemi", + "endLine": 4, + "endColumn": 1, + "fix": { "range": [ 46, 46 ], "text": ";" } + }, + { + "ruleId": "quotes", + "severity": 2, + "message": "Strings must use singlequote.", + "line": 4, + "column": 14, + "nodeType": "Literal", + "messageId": "wrongQuotes", + "endLine": 4, + "endColumn": 20, + "fix": { "range": [ 60, 66 ], "text": "'x = '" } + }, + { + "ruleId": "semi", + "severity": 2, + "message": "Missing semicolon.", + "line": 4, + "column": 25, + "nodeType": "ExpressionStatement", + "messageId": "missingSemi", + "endLine": 5, + "endColumn": 1, + "fix": { "range": [ 71, 71 ], "text": ";" } + } + ], + "errorCount": 4, + "warningCount": 0, + "fixableErrorCount": 4, + "fixableWarningCount": 0, + "source": "var x = 123;\n\nconsole.log( \"This is a thing\" )\nconsole.log( \"x = \", x )\n", + "usedDeprecatedRules": [] + } +] From fd64fa48f39b91e147ac7fd1bafba7c294f96aa8 Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Wed, 23 Jun 2021 17:49:03 +0300 Subject: [PATCH 6/8] E2E tests update readme with reporting info (#20151) * Updated e2e tests readme --- projects/plugins/jetpack/changelog/e2e-readme-update | 4 ++++ projects/plugins/jetpack/tests/e2e/README.md | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 projects/plugins/jetpack/changelog/e2e-readme-update diff --git a/projects/plugins/jetpack/changelog/e2e-readme-update b/projects/plugins/jetpack/changelog/e2e-readme-update new file mode 100644 index 0000000000000..a7a7ddbf671a5 --- /dev/null +++ b/projects/plugins/jetpack/changelog/e2e-readme-update @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +E2E tests: updated readme with reporting info diff --git a/projects/plugins/jetpack/tests/e2e/README.md b/projects/plugins/jetpack/tests/e2e/README.md index 41409089bb4f3..b9ab87f549011 100644 --- a/projects/plugins/jetpack/tests/e2e/README.md +++ b/projects/plugins/jetpack/tests/e2e/README.md @@ -1,3 +1,5 @@ +[![Reports status](https://img.shields.io/website?down_color=grey&down_message=Dashboard%20offline&style=for-the-badge&label=E2E%20TEST%20REPORTS&up_color=green&up_message=see%20dashboard&url=https%3A%2F%2Fautomattic.github.io%2Fjetpack-e2e-reports%2F%23%2F)](https://automattic.github.io/jetpack-e2e-reports) + # Jetpack End-to-End tests Automated end-to-end acceptance tests for the Jetpack plugin. @@ -14,6 +16,7 @@ Automated end-to-end acceptance tests for the Jetpack plugin. - [Writing tests](#writing-tests) - [Tests Architecture](#tests-architecture) - [CI configuration](#ci-configuration) +- [Test reports](#test-reports) ## Pre-requisites @@ -147,3 +150,7 @@ Tests rely on functionality plugins that provide some additional functionality, ### e2e-plan-data-interceptor.php The purpose of this plugin is to provide a way to `mock` Jetpack plan, for cases when we test functionality that does not directly use paid services. Great example of this purpose is a paid Gutenberg blocks. + +## Test reports + +Test reports are generated for every CI run and stored in [jetpack-e2e-reports](https://github.com/Automattic/jetpack-e2e-reports) repo. A dashboard displaying information about stored reports can be accessed at this link: [https://automattic.github.io/jetpack-e2e-reports](https://automattic.github.io/jetpack-e2e-reports) From 218acb216bfc24b2b2dd1122424cc1ce060e4be8 Mon Sep 17 00:00:00 2001 From: Samiff Date: Wed, 23 Jun 2021 11:01:16 -0600 Subject: [PATCH 7/8] Backup [Plugin]: Add GH issue template (#20144) --- .../ISSUE_TEMPLATE/plugin_jetpack-backup.md | 35 +++++++++++++++++++ ...elper.md => plugin_jetpack-debug-tools.md} | 0 2 files changed, 35 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/plugin_jetpack-backup.md rename .github/ISSUE_TEMPLATE/{plugin_debug-helper.md => plugin_jetpack-debug-tools.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/plugin_jetpack-backup.md b/.github/ISSUE_TEMPLATE/plugin_jetpack-backup.md new file mode 100644 index 0000000000000..a67df951c6500 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/plugin_jetpack-backup.md @@ -0,0 +1,35 @@ +--- +name: Plugin - Jetpack Backup +about: Create an issue report focused on the Jetpack Backup plugin +title: 'Backup [Plugin]: ADD_YOUR_TITLE_HERE' +labels: '[Plugin] Backup' +assignees: '' + +--- + + + +#### Steps to reproduce the issue + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See issue '....' + +#### What I expected + + +#### What happened instead + + +#### Screenshots + + + diff --git a/.github/ISSUE_TEMPLATE/plugin_debug-helper.md b/.github/ISSUE_TEMPLATE/plugin_jetpack-debug-tools.md similarity index 100% rename from .github/ISSUE_TEMPLATE/plugin_debug-helper.md rename to .github/ISSUE_TEMPLATE/plugin_jetpack-debug-tools.md From a6da953add5f137f2799898329dde1f3ea3ed454 Mon Sep 17 00:00:00 2001 From: Steve D <33553323+sdixon194@users.noreply.github.com> Date: Wed, 23 Jun 2021 14:17:10 -0400 Subject: [PATCH 8/8] CLI - Add Autotagger to Generate (#19818) --- tools/cli/commands/generate.js | 44 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/tools/cli/commands/generate.js b/tools/cli/commands/generate.js index c76e5b17db297..97beb84a401ab 100644 --- a/tools/cli/commands/generate.js +++ b/tools/cli/commands/generate.js @@ -182,8 +182,7 @@ export function getQuestions( type ) { { type: 'confirm', name: 'mirrorrepo', - message: - 'Add a mirror repo for a build version of the project to be automatically pushed to?', + message: 'Will this project require a mirror repo?', }, ]; const packageQuestions = []; @@ -390,40 +389,46 @@ async function mirrorRepo( composerJson, name, org = 'Automattic' ) { when: exists, // If the repo exists, confirm we want to use it. }, { + type: 'string', + name: 'newName', + message: 'What name do you want to use for the repo?', + when: newAnswers => exists && ! newAnswers.useExisting, // When there is an existing repo, but we don't want to use it. + }, + // Code for auto-adding repo to be added later. + /* { type: 'confirm', name: 'createNew', default: false, message: 'There is not an ' + repo + ' repo already. Shall I create one?', when: ! exists, // When the repo does not exist, do we want to ask to make it. - }, + }, */ + { - type: 'string', - name: 'newName', - message: 'What name do you want to use for the repo?', - when: newAnswers => exists && ! newAnswers.useExisting, // When there is an existing repo, but we don't want to use it. + type: 'confirm', + name: 'autotagger', + default: true, + message: 'Configure mirror repo to create new tags automatically (based on CHANGELOG.md)?', }, ] ); + /* if ( answers.createNew ) { // add function to create. console.log( - chalk.yellow( + chalk.bgBlue( 'We have not quite added the automatic creation of a mirror repo, so please visit https://github.com/organizations/Automattic/repositories/new to create a new repo of ' + name ) ); - await addMirrorRepo( composerJson, name, org ); - } else if ( answers.useExisting ) { - await addMirrorRepo( composerJson, name, org ); + await addMirrorRepo( composerJson, name, org, answers.autotagger ); + */ + if ( answers.useExisting ) { + await addMirrorRepo( composerJson, name, org, answers.autotagger ); } else if ( answers.newName ) { await mirrorRepo( composerJson, answers.newName, org ); // Rerun this function so we can check if the new name exists or not, etc. + } else { + await addMirrorRepo( composerJson, name, org, answers.autotagger ); } - - // Prompt: What repo would you like to use in the "org"? Default: "name". - - // Validate the name, then check for repo exists again. - - // If validated, add it to composerJson. If not repeat. } /** @@ -432,14 +437,17 @@ async function mirrorRepo( composerJson, name, org = 'Automattic' ) { * @param {object} composerJson - composer.json object. * @param {string} name - Repo name. * @param {string} org - Repo owner. + * @param {boolean} autotagger - if we want autotagger enabled. */ -function addMirrorRepo( composerJson, name, org ) { +function addMirrorRepo( composerJson, name, org, autotagger ) { composerJson.extra = composerJson.extra || {}; composerJson.extra[ 'mirror-repo' ] = org + '/' + name; composerJson.extra.changelogger = composerJson.extra.changelogger || {}; composerJson.extra.changelogger[ 'link-template' ] = `https://github.com/${ org }/${ name }/compare/v\${old}...v\${new}`; + // Add autotagger option + composerJson.extra.autotagger = autotagger; } /**