diff --git a/packages/cli/src/merge/handler.ts b/packages/cli/src/merge/handler.ts index eed5f527e..4c767df83 100644 --- a/packages/cli/src/merge/handler.ts +++ b/packages/cli/src/merge/handler.ts @@ -65,6 +65,7 @@ const mergeHandler = async (options: MergeOptions, logger: Logger) => { command: 'checkout', projectPath: commandPath, projectName: final.name || '', + log: options.log, }, logger ); diff --git a/packages/lexicon/core.d.ts b/packages/lexicon/core.d.ts index 8d0ae075f..267acf6bb 100644 --- a/packages/lexicon/core.d.ts +++ b/packages/lexicon/core.d.ts @@ -83,6 +83,9 @@ export type Workflow = { globals?: string; openfn?: OpenFnMetadata; + + // holds history information of a workflow + history?: string[] }; export type StepId = string; diff --git a/packages/project/src/Workflow.ts b/packages/project/src/Workflow.ts index 19a9b7574..721e51e43 100644 --- a/packages/project/src/Workflow.ts +++ b/packages/project/src/Workflow.ts @@ -30,6 +30,9 @@ class Workflow { this.workflow = clone(workflow); + // history needs to be on workflow object. + this.workflow.history = workflow.history?.length ? workflow.history : []; + const { id, name, openfn, steps, ...options } = workflow; if (!(id || name)) { throw new Error('A Workflow MUST have a name or id'); @@ -167,6 +170,22 @@ class Workflow { getVersionHash() { return generateHash(this); } + + pushHistory(versionHash: string) { + this.workflow.history?.push(versionHash); + } + + // return true if the current workflow can be merged into the target workflow without losing any changes + canMergeInto(target: Workflow) { + const thisHistory = this.workflow.history?.concat(this.getVersionHash()); + const targetHistory = target.workflow.history?.concat( + target.getVersionHash() + ); + + const targetHead = targetHistory[targetHistory.length - 1]; + if (thisHistory.indexOf(targetHead) > -1) return true; + return false; + } } export default Workflow; diff --git a/packages/project/test/gen/fixtures.ts b/packages/project/test/gen/fixtures.ts index e75597307..a33dd9725 100644 --- a/packages/project/test/gen/fixtures.ts +++ b/packages/project/test/gen/fixtures.ts @@ -4,6 +4,7 @@ export const ab = { id: 'workflow', name: 'Workflow', + history: [], steps: [ { id: 'a', diff --git a/packages/project/test/gen/generator.test.ts b/packages/project/test/gen/generator.test.ts index 794a0c59b..0c2ab6863 100644 --- a/packages/project/test/gen/generator.test.ts +++ b/packages/project/test/gen/generator.test.ts @@ -120,6 +120,7 @@ test('it should generate a simple workflow with any letter', (t) => { const expected = { id: 'workflow', name: 'Workflow', + history: [], steps: [ { id: 'x', @@ -152,6 +153,7 @@ test('it should generate a simple workflow with words, numbers and underscores', const expected = { id: 'workflow', name: 'Workflow', + history: [], steps: [ { id: 'node_1', @@ -184,6 +186,7 @@ test('it should generate two node pairs', (t) => { const expected = { id: 'workflow', name: 'Workflow', + history: [], steps: [ { id: 'a', @@ -231,6 +234,7 @@ test('it should generate two node pairs from one parent', (t) => { const expected = { id: 'workflow', name: 'Workflow', + history: [], steps: [ { id: 'a', @@ -276,6 +280,7 @@ test('it should generate several node pairs', (t) => { const expected = { id: 'workflow', name: 'Workflow', + history: [], steps: [ { id: 'a', @@ -439,6 +444,7 @@ test('it should generate a simple workflow with mapped uuids', (t) => { const expected = { id: 'workflow', name: 'Workflow', + history: [], steps: [ { id: 'a', @@ -481,6 +487,7 @@ test('it should generate a project with uuids', (t) => { const expected = { id: 'workflow', name: 'Workflow', + history: [], steps: [ { id: 'a', diff --git a/packages/project/test/parse/from-app-state.test.ts b/packages/project/test/parse/from-app-state.test.ts index 16fda2237..86c3ef6ed 100644 --- a/packages/project/test/parse/from-app-state.test.ts +++ b/packages/project/test/parse/from-app-state.test.ts @@ -114,6 +114,7 @@ test('should create a Project from prov state with a workflow', (t) => { t.deepEqual(project.workflows[0].toJSON(), { id: 'my-workflow', name: 'My Workflow', + history: [], steps: [ { id: 'trigger', diff --git a/packages/project/test/workflow.test.ts b/packages/project/test/workflow.test.ts index 82e7b914a..008db9f94 100644 --- a/packages/project/test/workflow.test.ts +++ b/packages/project/test/workflow.test.ts @@ -1,9 +1,11 @@ import { randomUUID } from 'node:crypto'; import test from 'ava'; import Workflow from '../src/Workflow'; +import { generateWorkflow } from '../src'; const simpleWorkflow = { id: 'my-workflow', + history: [], name: 'My Workflow', steps: [ { @@ -49,6 +51,11 @@ const simpleWorkflow = { }, }; +// should workflow.toJSON actually do this? +function realJson(v: any) { + return JSON.parse(JSON.stringify(v)); +} + test('create a Workflow from json', (t) => { const w = new Workflow(simpleWorkflow); @@ -205,3 +212,98 @@ test('map uuids to ids', (t) => { t.deepEqual(w.index.id[uuid_ac], 'a-c'); t.deepEqual(w.index.id[uuid_bc], 'b-c'); }); + +test('canMergeInto: should merge same content source & target', (t) => { + const main = generateWorkflow('trigger-x'); + const sbox = generateWorkflow('trigger-x'); + + t.true(sbox.canMergeInto(main)); +}); + +test("canMergeInto: shouldn't merge different content source & target", (t) => { + const main = generateWorkflow('trigger-x'); + const sbox = generateWorkflow('trigger-y'); + + t.false(sbox.canMergeInto(main)); +}); + +test('canMergeInto: source is target + changes', (t) => { + // initial main code + const main = generateWorkflow('trigger-x'); + main.pushHistory(main.getVersionHash()); + // main code updated + main.workflow.steps = generateWorkflow('trigger-x x-y').steps; + main.pushHistory(main.getVersionHash()); + + // clone main for sbox + const sbox = new Workflow(realJson(main.toJSON())); + // do code changes to sbox + sbox.workflow.steps = generateWorkflow('trigger-x x-y y-z').steps; + t.true(sbox.canMergeInto(main)); +}); + +test("canMergeInto: source isn't from target", (t) => { + // initial main code + const main = generateWorkflow('trigger-x'); + main.pushHistory(main.getVersionHash()); + // main code updated + main.workflow.steps = generateWorkflow('trigger-x x-y').steps; + main.pushHistory(main.getVersionHash()); + + // clone main for sbox + const sbox = generateWorkflow('trigger-y'); + sbox.pushHistory(sbox.getVersionHash()); + t.false(sbox.canMergeInto(main)); +}); + +test("canMergeInto: source isn't from target but ended with same code", (t) => { + // initial main code + const main = generateWorkflow('trigger-x'); + main.pushHistory(main.getVersionHash()); + // main code updated + main.workflow.steps = generateWorkflow('trigger-x x-y').steps; + main.pushHistory(main.getVersionHash()); + + const sbox = generateWorkflow('trigger-x x-y'); + t.true(sbox.canMergeInto(main)); +}); + +test('canMergeInto: source is from target but target has changes', (t) => { + // initial main code + const main = generateWorkflow('trigger-x'); + main.pushHistory(main.getVersionHash()); + // main code updated + main.workflow.steps = generateWorkflow('trigger-x x-y').steps; + main.pushHistory(main.getVersionHash()); + + // clone main for sbox + const sbox = new Workflow(realJson(main.toJSON())); + + // changes to main after cloning + main.workflow.steps = generateWorkflow('trigger-x x-y x-z').steps; + main.pushHistory(main.getVersionHash()); + + // merging sbox to main + t.false(sbox.canMergeInto(main)); +}); + +test('canMergeInto: source is from target but target & source have changes', (t) => { + // initial main code + const main = generateWorkflow('trigger-x'); + main.pushHistory(main.getVersionHash()); + // main code updated + main.workflow.steps = generateWorkflow('trigger-x x-y').steps; + main.pushHistory(main.getVersionHash()); + + // clone main for sbox + const sbox = new Workflow(realJson(main.toJSON())); + // changes to sbox + sbox.workflow.steps = generateWorkflow('trigger-x x-y y-g').steps; + + // changes to main after cloning + main.workflow.steps = generateWorkflow('trigger-x x-y x-z').steps; + main.pushHistory(main.getVersionHash()); + + // merging sbox to main + t.false(sbox.canMergeInto(main)); +});