fix(resolve): split up the resolve labels
This commit is contained in:
parent
c1b611ce38
commit
804c3bb133
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
| Function | Description |
|
| Function | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [resolveLabels(options)](./cautious-journey.resolvelabels.md) | Resolve the desired set of labels, given a starting set and the flags/states to be applied. |
|
| [resolveProject(options)](./cautious-journey.resolveproject.md) | |
|
||||||
| [syncIssueLabels(options)](./cautious-journey.syncissuelabels.md) | goes through and resolves each issue in the project. if there are changes and no errors, then updates the issue. |
|
| [syncIssueLabels(options)](./cautious-journey.syncissuelabels.md) | goes through and resolves each issue in the project. if there are changes and no errors, then updates the issue. |
|
||||||
| [syncProjectLabels(options)](./cautious-journey.syncprojectlabels.md) | |
|
| [syncProjectLabels(options)](./cautious-journey.syncprojectlabels.md) | |
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||||
|
|
||||||
[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [resolveLabels](./cautious-journey.resolvelabels.md)
|
[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [resolveProject](./cautious-journey.resolveproject.md)
|
||||||
|
|
||||||
## resolveLabels() function
|
## resolveProject() function
|
||||||
|
|
||||||
Resolve the desired set of labels, given a starting set and the flags/states to be applied.
|
|
||||||
|
|
||||||
<b>Signature:</b>
|
<b>Signature:</b>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export declare function resolveLabels(options: ResolveInput): ResolveResult;
|
export declare function resolveProject(options: ResolveInput): ResolveResult;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
|
@ -4,7 +4,7 @@ export { ChangeSet, FlagLabel, StateLabel, StateValue } from './labels';
|
||||||
export { Remote, RemoteOptions } from './remote';
|
export { Remote, RemoteOptions } from './remote';
|
||||||
export { GithubRemote } from './remote/github';
|
export { GithubRemote } from './remote/github';
|
||||||
export { GitlabRemote } from './remote/gitlab';
|
export { GitlabRemote } from './remote/gitlab';
|
||||||
export { ResolveInput, ResolveResult, resolveLabels } from './resolve';
|
export { ResolveInput, ResolveResult, resolveProject } from './resolve';
|
||||||
export { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync';
|
export { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync';
|
||||||
|
|
||||||
const STATUS_ERROR = 1;
|
const STATUS_ERROR = 1;
|
||||||
|
|
|
@ -16,7 +16,7 @@ export { FlagLabel, StateLabel } from './labels';
|
||||||
export { Remote, RemoteOptions } from './remote';
|
export { Remote, RemoteOptions } from './remote';
|
||||||
export { GithubRemote } from './remote/github';
|
export { GithubRemote } from './remote/github';
|
||||||
export { GitlabRemote } from './remote/gitlab';
|
export { GitlabRemote } from './remote/gitlab';
|
||||||
export { resolveLabels } from './resolve';
|
export { resolveProject } from './resolve';
|
||||||
export { syncIssueLabels, syncProjectLabels } from './sync';
|
export { syncIssueLabels, syncProjectLabels } from './sync';
|
||||||
|
|
||||||
const ARGS_START = 2;
|
const ARGS_START = 2;
|
||||||
|
|
241
src/resolve.ts
241
src/resolve.ts
|
@ -1,6 +1,6 @@
|
||||||
import { doesExist } from '@apextoaster/js-utils';
|
import { doesExist } from '@apextoaster/js-utils';
|
||||||
|
|
||||||
import { BaseLabel, FlagLabel, getValueName, prioritySort, StateLabel } from './labels';
|
import { BaseLabel, FlagLabel, getValueName, prioritySort, StateLabel, StateValue } from './labels';
|
||||||
import { defaultUntil } from './utils';
|
import { defaultUntil } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,131 +61,142 @@ export interface ResolveResult {
|
||||||
* Resolve the desired set of labels, given a starting set and the flags/states to be
|
* Resolve the desired set of labels, given a starting set and the flags/states to be
|
||||||
* applied.
|
* applied.
|
||||||
*/
|
*/
|
||||||
/* eslint-disable-next-line sonarjs/cognitive-complexity */
|
function resolveBaseLabel(label: BaseLabel, anticipatedResult: ResolveResult, activeLabels: Set<string>) {
|
||||||
export function resolveLabels(options: ResolveInput): ResolveResult {
|
if (activeLabels.has(label.name) === false) {
|
||||||
const activeLabels = new Set(options.labels);
|
return true;
|
||||||
const changes: Array<ChangeRecord> = [];
|
}
|
||||||
const errors: Array<ErrorRecord> = [];
|
|
||||||
|
for (const requiredLabel of label.requires) {
|
||||||
|
if (!activeLabels.has(requiredLabel.name)) {
|
||||||
|
if (activeLabels.delete(label.name)) {
|
||||||
|
anticipatedResult.changes.push({
|
||||||
|
cause: requiredLabel.name,
|
||||||
|
effect: ChangeVerb.REQUIRED,
|
||||||
|
label: label.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function checkLabelRules(label: BaseLabel) {
|
|
||||||
if (activeLabels.has(label.name) === false) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const requiredLabel of label.requires) {
|
|
||||||
if (!activeLabels.has(requiredLabel.name)) {
|
|
||||||
if (activeLabels.delete(label.name)) {
|
|
||||||
changes.push({
|
|
||||||
cause: requiredLabel.name,
|
|
||||||
effect: ChangeVerb.REQUIRED,
|
|
||||||
label: label.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const addedLabel of label.adds) {
|
|
||||||
// Set.add does not return a boolean, unlike the other methods
|
|
||||||
if (!activeLabels.has(addedLabel.name)) {
|
|
||||||
activeLabels.add(addedLabel.name);
|
|
||||||
changes.push({
|
|
||||||
cause: label.name,
|
|
||||||
effect: ChangeVerb.CREATED,
|
|
||||||
label: addedLabel.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const removedLabel of label.removes) {
|
|
||||||
if (activeLabels.delete(removedLabel.name)) {
|
|
||||||
changes.push({
|
|
||||||
cause: label.name,
|
|
||||||
effect: ChangeVerb.REMOVED,
|
|
||||||
label: removedLabel.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const addedLabel of label.adds) {
|
||||||
|
// Set.add does not return a boolean, unlike the other methods
|
||||||
|
if (!activeLabels.has(addedLabel.name)) {
|
||||||
|
activeLabels.add(addedLabel.name);
|
||||||
|
anticipatedResult.changes.push({
|
||||||
|
cause: label.name,
|
||||||
|
effect: ChangeVerb.CREATED,
|
||||||
|
label: addedLabel.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const removedLabel of label.removes) {
|
||||||
|
if (activeLabels.delete(removedLabel.name)) {
|
||||||
|
anticipatedResult.changes.push({
|
||||||
|
cause: label.name,
|
||||||
|
effect: ChangeVerb.REMOVED,
|
||||||
|
label: removedLabel.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBecomes(label: BaseLabel, anticipatedResult: ResolveResult, activeLabels: Set<string>, value: StateValue): boolean {
|
||||||
|
for (const become of value.becomes) {
|
||||||
|
const matches = become.matches.every((l) => activeLabels.has(l.name));
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
resolveBaseLabel({
|
||||||
|
...label,
|
||||||
|
adds: become.adds,
|
||||||
|
removes: [...become.matches, ...become.removes],
|
||||||
|
requires: [],
|
||||||
|
}, anticipatedResult, activeLabels);
|
||||||
|
|
||||||
|
if (activeLabels.delete(name)) {
|
||||||
|
anticipatedResult.changes.push({
|
||||||
|
cause: name,
|
||||||
|
effect: ChangeVerb.REMOVED,
|
||||||
|
label: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Need to ensure that there is only 1 active value for the state
|
||||||
|
* If no, remove any lower priority active values for the state
|
||||||
|
* Need to run the normal (add, remove) rules
|
||||||
|
* Need to run the becomes rules
|
||||||
|
*/
|
||||||
|
function resolveState(state: StateLabel, anticipatedResult: ResolveResult, activeLabels: Set<string>) {
|
||||||
|
let activeValue;
|
||||||
|
|
||||||
|
const sortedValues = prioritySort(state.values);
|
||||||
|
for (const value of sortedValues) {
|
||||||
|
const name = getValueName(state, value);
|
||||||
|
if (!activeLabels.has(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doesExist(activeValue)) { // there is already an active value
|
||||||
|
if (activeLabels.delete(name)) {
|
||||||
|
anticipatedResult.changes.push({
|
||||||
|
cause: name,
|
||||||
|
effect: ChangeVerb.CONFLICTED,
|
||||||
|
label: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedValue: BaseLabel = {
|
||||||
|
adds: [...state.adds, ...value.adds],
|
||||||
|
name,
|
||||||
|
priority: defaultUntil(value.priority, state.priority, 0),
|
||||||
|
removes: [...state.removes, ...value.removes],
|
||||||
|
requires: [...state.requires, ...value.requires],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resolveBaseLabel(combinedValue, anticipatedResult, activeLabels)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolveBecomes(combinedValue, anticipatedResult, activeLabels, value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeValue = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveProject(options: ResolveInput): ResolveResult {
|
||||||
|
const result: ResolveResult = {
|
||||||
|
changes: [],
|
||||||
|
errors: [],
|
||||||
|
labels: [],
|
||||||
|
};
|
||||||
|
const activeLabels = new Set(options.labels);
|
||||||
|
|
||||||
const sortedFlags = prioritySort(options.flags);
|
const sortedFlags = prioritySort(options.flags);
|
||||||
for (const flag of sortedFlags) {
|
for (const flag of sortedFlags) {
|
||||||
checkLabelRules(flag);
|
resolveBaseLabel(flag, result, activeLabels);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedStates = prioritySort(options.states);
|
const sortedStates = prioritySort(options.states);
|
||||||
for (const state of sortedStates) {
|
for (const state of sortedStates) {
|
||||||
let activeValue;
|
resolveState(state, result, activeLabels);
|
||||||
|
|
||||||
const sortedValues = prioritySort(state.values);
|
|
||||||
for (const value of sortedValues) {
|
|
||||||
const name = getValueName(state, value);
|
|
||||||
if (activeLabels.has(name)) {
|
|
||||||
if (doesExist(activeValue)) {
|
|
||||||
if (activeLabels.delete(name)) {
|
|
||||||
changes.push({
|
|
||||||
cause: name,
|
|
||||||
effect: ChangeVerb.CONFLICTED,
|
|
||||||
label: name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const combinedValue: BaseLabel = {
|
|
||||||
adds: [...state.adds, ...value.adds],
|
|
||||||
name,
|
|
||||||
priority: defaultUntil(value.priority, state.priority, 0),
|
|
||||||
removes: [...state.removes, ...value.removes],
|
|
||||||
requires: [...state.requires, ...value.requires],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (checkLabelRules(combinedValue)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: flatten this bit and remove the mutable boolean
|
|
||||||
let removed = false;
|
|
||||||
for (const become of value.becomes) {
|
|
||||||
const matches = become.matches.every((l) => activeLabels.has(l.name));
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
checkLabelRules({
|
|
||||||
...combinedValue,
|
|
||||||
adds: become.adds,
|
|
||||||
removes: [...become.matches, ...become.removes],
|
|
||||||
requires: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (activeLabels.delete(name)) {
|
|
||||||
changes.push({
|
|
||||||
cause: name,
|
|
||||||
effect: ChangeVerb.REMOVED,
|
|
||||||
label: name,
|
|
||||||
});
|
|
||||||
removed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeValue = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
result.labels = Array.from(activeLabels);
|
||||||
changes,
|
|
||||||
errors,
|
return result;
|
||||||
labels: Array.from(activeLabels),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { prng } from 'seedrandom';
|
||||||
import { ProjectConfig } from './config';
|
import { ProjectConfig } from './config';
|
||||||
import { getLabelColor, getLabelNames, getValueName } from './labels';
|
import { getLabelColor, getLabelNames, getValueName } from './labels';
|
||||||
import { LabelUpdate, Remote } from './remote';
|
import { LabelUpdate, Remote } from './remote';
|
||||||
import { resolveLabels } from './resolve';
|
import { resolveProject } from './resolve';
|
||||||
import { compareItems, defaultTo, defaultUntil } from './utils';
|
import { compareItems, defaultTo, defaultUntil } from './utils';
|
||||||
|
|
||||||
export interface SyncOptions {
|
export interface SyncOptions {
|
||||||
|
@ -28,7 +28,7 @@ export async function syncIssueLabels(options: SyncOptions): Promise<unknown> {
|
||||||
for (const issue of issues) {
|
for (const issue of issues) {
|
||||||
logger.info({ issue }, 'project issue');
|
logger.info({ issue }, 'project issue');
|
||||||
|
|
||||||
const { changes, errors, labels } = resolveLabels({
|
const { changes, errors, labels } = resolveProject({
|
||||||
flags: project.flags,
|
flags: project.flags,
|
||||||
labels: issue.labels,
|
labels: issue.labels,
|
||||||
states: project.states,
|
states: project.states,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { resolveLabels } from '../src/resolve';
|
import { resolveProject } from '../src/resolve';
|
||||||
import { TEST_CASES } from './resolve/cases';
|
import { TEST_CASES } from './resolve/cases';
|
||||||
|
|
||||||
const TEST_LABELS = ['foo', 'bar'];
|
const TEST_LABELS = ['foo', 'bar'];
|
||||||
|
@ -8,7 +8,7 @@ const TEST_LABELS = ['foo', 'bar'];
|
||||||
describe('resolve labels', () => {
|
describe('resolve labels', () => {
|
||||||
describe('with empty rule set', () => {
|
describe('with empty rule set', () => {
|
||||||
it('should return the existing labels', () => {
|
it('should return the existing labels', () => {
|
||||||
const result = resolveLabels({
|
const result = resolveProject({
|
||||||
flags: [],
|
flags: [],
|
||||||
labels: TEST_LABELS,
|
labels: TEST_LABELS,
|
||||||
states: [],
|
states: [],
|
||||||
|
@ -18,7 +18,7 @@ describe('resolve labels', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not make any changes', () => {
|
it('should not make any changes', () => {
|
||||||
const result = resolveLabels({
|
const result = resolveProject({
|
||||||
flags: [],
|
flags: [],
|
||||||
labels: TEST_LABELS,
|
labels: TEST_LABELS,
|
||||||
states: [],
|
states: [],
|
||||||
|
@ -32,7 +32,7 @@ describe('resolve labels', () => {
|
||||||
describe('resolver test cases', () => {
|
describe('resolver test cases', () => {
|
||||||
for (const test of TEST_CASES) {
|
for (const test of TEST_CASES) {
|
||||||
it(`should resolve ${test.name}`, () => {
|
it(`should resolve ${test.name}`, () => {
|
||||||
const actualResult = resolveLabels(test.input);
|
const actualResult = resolveProject(test.input);
|
||||||
expect(actualResult).to.deep.equal(test.result);
|
expect(actualResult).to.deep.equal(test.result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { resolveLabels } from '../../src/resolve';
|
import { resolveProject } from '../../src/resolve';
|
||||||
|
|
||||||
const TEST_LABELS = ['foo', 'bar'];
|
const TEST_LABELS = ['foo', 'bar'];
|
||||||
|
|
||||||
describe('resolve labels', () => {
|
describe('resolve labels', () => {
|
||||||
describe('flags with unfulfilled requires rule', () => {
|
describe('flags with unfulfilled requires rule', () => {
|
||||||
it('should be removed when required label is missing', () => {
|
it('should be removed when required label is missing', () => {
|
||||||
const result = resolveLabels({
|
const result = resolveProject({
|
||||||
flags: [{
|
flags: [{
|
||||||
adds: [],
|
adds: [],
|
||||||
name: 'gayle',
|
name: 'gayle',
|
||||||
|
@ -27,7 +27,7 @@ describe('resolve labels', () => {
|
||||||
|
|
||||||
describe('flags with fulfilled requires rule', () => {
|
describe('flags with fulfilled requires rule', () => {
|
||||||
it('should make no changes', () => {
|
it('should make no changes', () => {
|
||||||
const result = resolveLabels({
|
const result = resolveProject({
|
||||||
flags: [{
|
flags: [{
|
||||||
adds: [],
|
adds: [],
|
||||||
name: 'gayle',
|
name: 'gayle',
|
||||||
|
@ -47,7 +47,7 @@ describe('resolve labels', () => {
|
||||||
|
|
||||||
describe('flags with add rules', () => {
|
describe('flags with add rules', () => {
|
||||||
it('should add the labels', () => {
|
it('should add the labels', () => {
|
||||||
const result = resolveLabels({
|
const result = resolveProject({
|
||||||
flags: [{
|
flags: [{
|
||||||
adds: [{
|
adds: [{
|
||||||
name: 'linda',
|
name: 'linda',
|
||||||
|
@ -67,7 +67,7 @@ describe('resolve labels', () => {
|
||||||
|
|
||||||
describe('flags with remove rules', () => {
|
describe('flags with remove rules', () => {
|
||||||
it('should remove labels', () => {
|
it('should remove labels', () => {
|
||||||
const result = resolveLabels({
|
const result = resolveProject({
|
||||||
flags: [{
|
flags: [{
|
||||||
adds: [],
|
adds: [],
|
||||||
name: 'bob',
|
name: 'bob',
|
||||||
|
|
Loading…
Reference in New Issue