From 156c80360e8e60806514bd216cc227209280fbd9 Mon Sep 17 00:00:00 2001 From: Andrew Top Date: Fri, 4 Oct 2024 04:52:17 -0400 Subject: [PATCH] #2426 Allow import branch specification (#2432) --- src/git-utils.js | 4 +- src/import.cmd.js | 70 +++++++++++++++---- src/import.js | 2 +- test/import-cmd.test.js | 149 +++++++++++++++++++++++++++++++++++++++- test/utils.js | 12 ++++ 5 files changed, 220 insertions(+), 17 deletions(-) diff --git a/src/git-utils.js b/src/git-utils.js index c96a2f66d..0f01be361 100644 --- a/src/git-utils.js +++ b/src/git-utils.js @@ -23,6 +23,8 @@ import git from 'isomorphic-git'; const cache = {}; export default class GitUtils { + static DEFAULT_BRANCH = 'main'; + /** * Determines whether the working tree directory contains uncommitted or unstaged changes. * @@ -141,7 +143,7 @@ export default class GitUtils { * @param {string} fallback fallback value if no branch or tag is found * @returns {Promise} current branch or tag */ - static async getBranch(dir, fallback = 'main') { + static async getBranch(dir, fallback = this.DEFAULT_BRANCH) { // current commit sha const rev = await git.resolveRef({ fs, dir, ref: 'HEAD' }); // reverse-lookup tag from commit sha diff --git a/src/import.cmd.js b/src/import.cmd.js index f7fac7169..7aa235d08 100644 --- a/src/import.cmd.js +++ b/src/import.cmd.js @@ -14,6 +14,7 @@ import path from 'path'; import chalk from 'chalk-template'; import git from 'isomorphic-git'; import http from 'isomorphic-git/http/node/index.js'; +import GitUtils from './git-utils.js'; import { HelixImportProject } from './server/HelixImportProject.js'; import pkgJson from './package.cjs'; import { AbstractServerCommand } from './abstract-server.cmd.js'; @@ -36,7 +37,20 @@ export default class ImportCommand extends AbstractServerCommand { } withUIRepo(value) { - this._uiRepo = value; + this._uiBranch = GitUtils.DEFAULT_BRANCH; + try { + const url = new URL(value); + if (url.hash) { + this._uiBranch = url.hash.substring(1); + } + // Ensure a dangling hash does not create an invalid uiRepo. + url.hash = ''; + this._uiRepo = url.href; + } catch (e) { + this.log.error(`Could not process UI Repo correctly: ${value} - ${e.message}`); + throw e; + } + return this; } @@ -45,6 +59,27 @@ export default class ImportCommand extends AbstractServerCommand { return this; } + /** + * Ensure the UI branch the caller specified (or defaulted to) is the one being used. + * @param uiFolder + * @returns {Promise} + */ + async handleUIBranch(uiFolder) { + const branch = await GitUtils.getBranch(uiFolder); + + if (branch !== this._uiBranch) { + this.log.info( + chalk`AEM Importer UI was on branch {yellow ${branch}}. Switching to {green ${this._uiBranch}}.`, + ); + await git.checkout({ + fs: fse, + http, + dir: uiFolder, + ref: this._uiBranch, + }); + } + } + async setupImporterUI() { const importerFolder = path.join(this.directory, this._importerSubPath); await fse.ensureDir(importerFolder); @@ -54,32 +89,41 @@ export default class ImportCommand extends AbstractServerCommand { const exists = await fse.pathExists(uiFolder); if (!exists) { this.log.info('AEM Importer UI needs to be installed.'); - this.log.info(`Cloning ${this._uiRepo} in ${importerFolder}.`); + this.log.info(`Cloning ${this._uiRepo} (branch: ${this._uiBranch}) in ${importerFolder}.`); // clone the ui project - await git.clone({ - fs: fse, - http, - dir: uiFolder, - url: this._uiRepo, - depth: 1, - singleBranch: true, - }); - this.log.info(`AEM Importer UI is ready. v${await getUIVersion()}`); + try { + await git.clone({ + fs: fse, + http, + dir: uiFolder, + url: this._uiRepo, + ref: this._uiBranch, + depth: 1, + }); + + this.log.info(`AEM Importer UI is ready. v${await getUIVersion()} (branch: ${this._uiBranch})`); + } catch (e) { + this.log.error(`AEM Importer UI clone failed: ${e.message}`); + throw e; + } } else { this.log.info('Fetching latest version of the AEM Importer UI...'); - // clone the ui project + await this.handleUIBranch(uiFolder); + + // Pull the ui project await git.pull({ fs: fse, http, dir: uiFolder, url: this._uiRepo, + ref: this._uiBranch, depth: 1, singleBranch: true, author: { name: 'hlx import', }, }); - this.log.info(`AEM Importer UI is up-to-date. v${await getUIVersion()}`); + this.log.info(`AEM Importer UI is up-to-date. v${await getUIVersion()} (branch: ${this._uiBranch})`); } } diff --git a/src/import.js b/src/import.js index 94274f905..78f15fe4d 100644 --- a/src/import.js +++ b/src/import.js @@ -29,7 +29,7 @@ export default function up() { }) .option('ui-repo', { alias: 'uiRepo', - describe: 'Git repository for the AEM Importer UI', + describe: 'Git repository for the AEM Importer UI. A fragment may be used to indicated a branch other than main.', type: 'string', default: 'https://github.com/adobe/helix-importer-ui', }) diff --git a/test/import-cmd.test.js b/test/import-cmd.test.js index f0358fcab..3cebed5fa 100644 --- a/test/import-cmd.test.js +++ b/test/import-cmd.test.js @@ -15,7 +15,13 @@ import assert from 'assert'; import path from 'path'; import fse from 'fs-extra'; import { h1NoCache as fetchContext } from '@adobe/fetch'; -import { Nock, assertHttp, createTestRoot } from './utils.js'; +import GitUtils from '../src/git-utils.js'; +import { + Nock, + assertHttp, + createTestRoot, + getBranch, +} from './utils.js'; import ImportCommand from '../src/import.cmd.js'; const { fetch } = fetchContext({ rejectUnauthorized: false }); @@ -558,7 +564,7 @@ describe('Import command - importer ui', function suite() { this.timeout(240000); - it('import command installs the importer ui', (done) => { + it('import command installs the main importer ui', (done) => { let error = null; const cmd = new ImportCommand() .withDirectory(testDir) @@ -576,6 +582,7 @@ describe('Import command - importer ui', function suite() { try { assert.ok(await fse.pathExists(`${testDir}/tools/importer/helix-importer-ui/index.html`), 'helix-importer-ui project has been cloned'); await assertHttp(`http://127.0.0.1:${cmd.project.server.port}/tools/importer/helix-importer-ui/index.html`, 200); + assert.equal(getBranch(`${testDir}/tools/importer/helix-importer-ui`), 'main'); await myDone(); } catch (e) { @@ -588,4 +595,142 @@ describe('Import command - importer ui', function suite() { .run() .catch(done); }); + + it('import command installs a importer ui test branch', (done) => { + // This assumes the branch 'origin/test' will exist and be available. + let error = null; + const cmd = new ImportCommand() + .withDirectory(testDir) + .withOpen(false) + .withUIRepo('https://github.com/adobe/helix-importer-ui#test') + .withHttpPort(0); + + const myDone = (err) => { + error = err; + return cmd.stop(); + }; + + cmd + .on('started', async () => { + try { + assert.ok(await fse.pathExists(`${testDir}/tools/importer/helix-importer-ui/index.html`), 'helix-importer-ui project has been cloned'); + await assertHttp(`http://127.0.0.1:${cmd.project.server.port}/tools/importer/helix-importer-ui/index.html`, 200); + assert.equal(getBranch(`${testDir}/tools/importer/helix-importer-ui`), 'test'); + + await myDone(); + } catch (e) { + await myDone(e); + } + }) + .on('stopped', () => { + done(error); + }) + .run() + .catch(done); + }); + + it('import command fails to install a bad branch', (done) => { + // This assumes the branch 'origin/bad' does not exist. + let error = null; + const cmd = new ImportCommand() + .withDirectory(testDir) + .withOpen(false) + .withUIRepo('https://github.com/adobe/helix-importer-ui#bad') + .withHttpPort(0); + + const myDone = (err) => { + error = err; + return cmd.stop(); + }; + + cmd + .on('started', async () => { + try { + assert.fail('Should not have been able to clone the importer ui'); + await myDone(); + } catch (e) { + await myDone(e); + } + }) + .on('stopped', () => { + done(error); + }) + .run() + .catch(() => { + done(); + }); + }); + + it('import command fails to install an invalid ui-repo', () => { + try { + new ImportCommand() + .withDirectory(testDir) + .withOpen(false) + .withUIRepo('not a url#hash') + .withHttpPort(0); + assert.fail('Importer UI repo URL was invalid - exception expected.'); + } catch (e) { + assert.equal(e.message, 'Invalid URL'); + } + }); + + /** + * Testing starting the importer on the default (main) branch, and switching it to a test + * branch. + */ + it('import command handles an empty branch', () => { + try { + const cmd = new ImportCommand() + .withDirectory(testDir) + .withOpen(false) + .withUIRepo('https://github.com/adobe/helix-importer-ui#') + .withHttpPort(0); + // eslint-disable-next-line no-underscore-dangle + assert.equal(cmd._uiRepo, 'https://github.com/adobe/helix-importer-ui'); + // eslint-disable-next-line no-underscore-dangle + assert.equal(cmd._uiBranch, GitUtils.DEFAULT_BRANCH); + } catch (e) { + assert.fail('Importer UI repo URL was valid - exception not expected.'); + } + }); + + it('import command switches branch', (done) => { + const cmd = new ImportCommand() + .withDirectory(testDir) + .withOpen(false) + .withUIRepo('https://github.com/adobe/helix-importer-ui#') + .withHttpPort(0); + cmd + .on('started', async () => { + const mainBranch = await getBranch(`${testDir}/tools/importer/helix-importer-ui`); + assert.equal(mainBranch, 'main'); + + await cmd.stop(); + }) + .on('stopped', async () => { + // Main branch server has stopped. Now switch it to 'test'. + const cmd2 = new ImportCommand() + .withDirectory(testDir) + .withOpen(false) + .withUIRepo('https://github.com/adobe/helix-importer-ui#test') + .withHttpPort(0); + cmd2 + .on('started', async () => { + const testBranch = await getBranch(`${testDir}/tools/importer/helix-importer-ui`); + assert.equal(testBranch, 'test'); + await cmd2.stop(); + }) + .on('stopped', () => { + done(); + }) + .run() + .catch((e) => { + done(e); + }); + }) + .run() + .catch((e) => { + done(e); + }); + }); }); diff --git a/test/utils.js b/test/utils.js index 6871272e5..9f91e79c0 100644 --- a/test/utils.js +++ b/test/utils.js @@ -43,9 +43,21 @@ export function switchBranch(dir, branch) { shell.cd(dir); shell.exec(`git checkout -b ${branch}`); shell.cd(pwd); + // eslint-disable-next-line no-console console.log(`switched to branch ${branch} in ${dir}`); } +export function getBranch(dir) { + const pwd = shell.pwd(); + shell.cd(dir); + const { stdout } = shell.exec('git rev-parse --abbrev-ref HEAD'); + shell.cd(pwd); + // eslint-disable-next-line no-console + console.log(`The current branch is ${stdout.trim()} in ${dir}`); + + return stdout.trim(); +} + export function clearHelixEnv() { const deleted = {}; Object.keys(process.env).filter((key) => key.startsWith(('AEM_'))).forEach((key) => {