1
0
Fork 0

fix(resolve): split up the resolve labels

This commit is contained in:
bzlibby 2020-08-29 00:18:35 -05:00 committed by Sean Sube
parent c1b611ce38
commit 804c3bb133
8 changed files with 143 additions and 134 deletions

View File

@ -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) | |

View File

@ -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) &gt; [cautious-journey](./cautious-journey.md) &gt; [resolveLabels](./cautious-journey.resolvelabels.md) [Home](./index.md) &gt; [cautious-journey](./cautious-journey.md) &gt; [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

View File

@ -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;

View File

@ -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;

View File

@ -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),
};
} }

View File

@ -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,

View File

@ -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);
}); });
} }

View File

@ -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',