@pgpmjs/core 7.31.0 → 7.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Insert `entry` into every test matrix of a workflow, keeping the list sorted.
3
+ * Only the byte range of each matrix sequence is rewritten; the rest of the
4
+ * source passes through untouched, so comments and formatting survive.
5
+ */
6
+ export declare const addToMatrixYaml: (source: string, entry: string) => string;
7
+ /**
8
+ * Add a module to the test matrix of a workspace's CI workflows, keeping the
9
+ * list sorted. The matrix stays a plain, hand-editable array: workflows without
10
+ * a `jobs.<job>.strategy.matrix.package` sequence are left alone.
11
+ * Best-effort: unreadable or unwritable workflows are skipped, never thrown.
12
+ *
13
+ * Returns the workflow files that changed, relative to `workspacePath`.
14
+ */
15
+ export declare const addToCiMatrix: (workspacePath: string, modulePackagePath: string) => string[];
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.addToCiMatrix = exports.addToMatrixYaml = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const yaml_1 = require("yaml");
10
+ const WORKFLOW_DIR = path_1.default.join('.github', 'workflows');
11
+ const MATRIX_KEY = 'package';
12
+ /** Column the node starts at, i.e. the indent of its first line. */
13
+ const columnOf = (source, offset) => offset - (source.lastIndexOf('\n', offset - 1) + 1);
14
+ /** Every `jobs.<job>.strategy.matrix.package` sequence in a workflow. */
15
+ const findMatrixSeqs = (doc) => {
16
+ const jobs = doc.get('jobs', true);
17
+ if (!(0, yaml_1.isMap)(jobs))
18
+ return [];
19
+ const seqs = [];
20
+ for (const job of jobs.items) {
21
+ if (!(0, yaml_1.isScalar)(job.key))
22
+ continue;
23
+ const seq = doc.getIn(['jobs', job.key.value, 'strategy', 'matrix', MATRIX_KEY], true);
24
+ if ((0, yaml_1.isSeq)(seq))
25
+ seqs.push(seq);
26
+ }
27
+ return seqs;
28
+ };
29
+ /**
30
+ * The sequence's entries with their original source text (so quoting survives),
31
+ * or `null` if any item isn't a plain string scalar.
32
+ */
33
+ const entriesOf = (seq, source) => {
34
+ const entries = [];
35
+ for (const item of seq.items) {
36
+ if (!(0, yaml_1.isScalar)(item) || typeof item.value !== 'string') {
37
+ return null;
38
+ }
39
+ const range = item.range;
40
+ if (!range)
41
+ return null;
42
+ const [start, end] = range;
43
+ entries.push({
44
+ value: item.value,
45
+ text: source.slice(start, end).trim(),
46
+ comment: item.comment,
47
+ commentBefore: item.commentBefore
48
+ });
49
+ }
50
+ return entries;
51
+ };
52
+ const commentLines = (comment) => comment ? comment.split('\n').filter((line) => line !== '') : [];
53
+ /** Find standalone comment lines immediately before a node. */
54
+ const commentBlockStart = (source, offset) => {
55
+ let lineStart = source.lastIndexOf('\n', offset - 1) + 1;
56
+ let first;
57
+ while (lineStart > 0) {
58
+ const lineEnd = lineStart - (source[lineStart - 1] === '\n' ? 1 : 0);
59
+ const previousStart = source.lastIndexOf('\n', lineEnd - 1) + 1;
60
+ const line = source.slice(previousStart, lineEnd).replace(/\r$/, '');
61
+ if (!/^\s*#/.test(line))
62
+ break;
63
+ first = previousStart;
64
+ lineStart = previousStart;
65
+ }
66
+ return first;
67
+ };
68
+ const renderCommentBefore = (comment, indent) => commentLines(comment)
69
+ .map((line) => `${indent}#${line}`)
70
+ .join('\n');
71
+ const commentsOf = (doc) => {
72
+ const comments = [];
73
+ const add = (comment, multiline = false) => {
74
+ if (comment === null || comment === undefined)
75
+ return;
76
+ const values = multiline ? comment.split('\n') : [comment];
77
+ comments.push(...values.map((value) => value.trim()));
78
+ };
79
+ add(doc.comment);
80
+ add(doc.commentBefore, true);
81
+ if (doc.contents) {
82
+ (0, yaml_1.visit)(doc.contents, (_key, node) => {
83
+ if (!node || typeof node !== 'object')
84
+ return;
85
+ const commented = node;
86
+ add(commented.comment);
87
+ add(commented.commentBefore, true);
88
+ });
89
+ }
90
+ return comments.sort();
91
+ };
92
+ /** Re-render a sequence in the style and at the indent it was written with. */
93
+ const renderSeq = (seq, entries, source, leadingComment = false) => {
94
+ const texts = entries.map((entry) => entry.text);
95
+ if (seq.flow) {
96
+ const original = seq.range
97
+ ? source.slice(seq.range[0], seq.range[1])
98
+ : '';
99
+ if (!original.includes('\n'))
100
+ return `[${texts.join(', ')}]`;
101
+ const firstItem = seq.items[0];
102
+ if (!(0, yaml_1.isScalar)(firstItem) || !firstItem.range) {
103
+ return `[${texts.join(', ')}]`;
104
+ }
105
+ const itemIndent = ' '.repeat(columnOf(source, firstItem.range[0]));
106
+ const closing = original.lastIndexOf(']');
107
+ const closingIndent = ' '.repeat(closing >= 0 ? columnOf(source, seq.range[0] + closing) : columnOf(source, seq.range[0]));
108
+ return `[\n${texts
109
+ .map((text, index) => `${itemIndent}${text}${index === texts.length - 1 ? '' : ','}`)
110
+ .join('\n')}\n${closingIndent}]`;
111
+ }
112
+ const indent = ' '.repeat(columnOf(source, seq.range[0]));
113
+ const rendered = entries.map((entry, index) => {
114
+ const before = renderCommentBefore(entry.commentBefore, indent);
115
+ const itemIndent = index === 0 && !entry.commentBefore && !leadingComment ? '' : indent;
116
+ const item = `${itemIndent}- ${entry.text}${entry.comment ? ` #${entry.comment}` : ''}`;
117
+ return before ? `${before}\n${item}` : item;
118
+ });
119
+ return rendered.join('\n');
120
+ };
121
+ /**
122
+ * Insert `entry` into every test matrix of a workflow, keeping the list sorted.
123
+ * Only the byte range of each matrix sequence is rewritten; the rest of the
124
+ * source passes through untouched, so comments and formatting survive.
125
+ */
126
+ const addToMatrixYaml = (source, entry) => {
127
+ const doc = (0, yaml_1.parseDocument)(source);
128
+ if (doc.errors.length)
129
+ return source;
130
+ const added = {
131
+ value: entry,
132
+ text: (0, yaml_1.stringify)(entry).trim()
133
+ };
134
+ const edits = [];
135
+ for (const seq of findMatrixSeqs(doc)) {
136
+ const entries = entriesOf(seq, source);
137
+ if (!entries)
138
+ continue;
139
+ if (entries.some((existing) => existing.value === entry))
140
+ continue;
141
+ if (!seq.range)
142
+ continue;
143
+ if (seq.flow &&
144
+ (entries.some((existing) => existing.comment || existing.commentBefore) ||
145
+ seq.comment ||
146
+ seq.commentBefore)) {
147
+ continue;
148
+ }
149
+ const [originalStart, end] = seq.range;
150
+ const firstCommentStart = !seq.flow && (seq.commentBefore || entries[0]?.commentBefore)
151
+ ? commentBlockStart(source, originalStart)
152
+ : undefined;
153
+ const entriesWithComments = firstCommentStart === undefined || !seq.commentBefore || !entries[0]
154
+ ? entries
155
+ : [
156
+ {
157
+ ...entries[0],
158
+ commentBefore: entries[0].commentBefore
159
+ ? `${seq.commentBefore}\n${entries[0].commentBefore}`
160
+ : seq.commentBefore
161
+ },
162
+ ...entries.slice(1)
163
+ ];
164
+ const next = [...entriesWithComments, added]
165
+ .filter((item, index, all) => all.findIndex((other) => other.value === item.value) === index)
166
+ .sort((left, right) => left.value.localeCompare(right.value));
167
+ const start = firstCommentStart ?? originalStart;
168
+ // a block sequence's range extends to the next token; keep that whitespace
169
+ const [trailing] = /\s*$/.exec(source.slice(originalStart, end));
170
+ const rendered = renderSeq(seq, next, source, firstCommentStart !== undefined);
171
+ const replacement = rendered + trailing;
172
+ const candidate = source.slice(0, start) + replacement + source.slice(end);
173
+ const candidateDoc = (0, yaml_1.parseDocument)(candidate);
174
+ if (candidateDoc.errors.length ||
175
+ commentsOf(candidateDoc).join('\0') !== commentsOf(doc).join('\0')) {
176
+ continue;
177
+ }
178
+ edits.push({
179
+ start,
180
+ end,
181
+ text: replacement
182
+ });
183
+ }
184
+ // Last edit first, so earlier ranges keep their offsets.
185
+ return edits
186
+ .sort((left, right) => right.start - left.start)
187
+ .reduce((text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end), source);
188
+ };
189
+ exports.addToMatrixYaml = addToMatrixYaml;
190
+ /**
191
+ * Add a module to the test matrix of a workspace's CI workflows, keeping the
192
+ * list sorted. The matrix stays a plain, hand-editable array: workflows without
193
+ * a `jobs.<job>.strategy.matrix.package` sequence are left alone.
194
+ * Best-effort: unreadable or unwritable workflows are skipped, never thrown.
195
+ *
196
+ * Returns the workflow files that changed, relative to `workspacePath`.
197
+ */
198
+ const addToCiMatrix = (workspacePath, modulePackagePath) => {
199
+ const dir = path_1.default.join(workspacePath, WORKFLOW_DIR);
200
+ const entry = modulePackagePath.split(path_1.default.sep).join('/');
201
+ const changed = [];
202
+ try {
203
+ if (!fs_1.default.existsSync(dir))
204
+ return changed;
205
+ for (const file of fs_1.default.readdirSync(dir).sort()) {
206
+ if (!/\.ya?ml$/.test(file))
207
+ continue;
208
+ try {
209
+ const filePath = path_1.default.join(dir, file);
210
+ const original = fs_1.default.readFileSync(filePath, 'utf8');
211
+ const updated = (0, exports.addToMatrixYaml)(original, entry);
212
+ if (updated === original)
213
+ continue;
214
+ fs_1.default.writeFileSync(filePath, updated);
215
+ changed.push(path_1.default.join(WORKFLOW_DIR, file).split(path_1.default.sep).join('/'));
216
+ }
217
+ catch {
218
+ continue;
219
+ }
220
+ }
221
+ }
222
+ catch {
223
+ return changed;
224
+ }
225
+ return changed;
226
+ };
227
+ exports.addToCiMatrix = addToCiMatrix;
@@ -0,0 +1,219 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from 'yaml';
4
+ const WORKFLOW_DIR = path.join('.github', 'workflows');
5
+ const MATRIX_KEY = 'package';
6
+ /** Column the node starts at, i.e. the indent of its first line. */
7
+ const columnOf = (source, offset) => offset - (source.lastIndexOf('\n', offset - 1) + 1);
8
+ /** Every `jobs.<job>.strategy.matrix.package` sequence in a workflow. */
9
+ const findMatrixSeqs = (doc) => {
10
+ const jobs = doc.get('jobs', true);
11
+ if (!isMap(jobs))
12
+ return [];
13
+ const seqs = [];
14
+ for (const job of jobs.items) {
15
+ if (!isScalar(job.key))
16
+ continue;
17
+ const seq = doc.getIn(['jobs', job.key.value, 'strategy', 'matrix', MATRIX_KEY], true);
18
+ if (isSeq(seq))
19
+ seqs.push(seq);
20
+ }
21
+ return seqs;
22
+ };
23
+ /**
24
+ * The sequence's entries with their original source text (so quoting survives),
25
+ * or `null` if any item isn't a plain string scalar.
26
+ */
27
+ const entriesOf = (seq, source) => {
28
+ const entries = [];
29
+ for (const item of seq.items) {
30
+ if (!isScalar(item) || typeof item.value !== 'string') {
31
+ return null;
32
+ }
33
+ const range = item.range;
34
+ if (!range)
35
+ return null;
36
+ const [start, end] = range;
37
+ entries.push({
38
+ value: item.value,
39
+ text: source.slice(start, end).trim(),
40
+ comment: item.comment,
41
+ commentBefore: item.commentBefore
42
+ });
43
+ }
44
+ return entries;
45
+ };
46
+ const commentLines = (comment) => comment ? comment.split('\n').filter((line) => line !== '') : [];
47
+ /** Find standalone comment lines immediately before a node. */
48
+ const commentBlockStart = (source, offset) => {
49
+ let lineStart = source.lastIndexOf('\n', offset - 1) + 1;
50
+ let first;
51
+ while (lineStart > 0) {
52
+ const lineEnd = lineStart - (source[lineStart - 1] === '\n' ? 1 : 0);
53
+ const previousStart = source.lastIndexOf('\n', lineEnd - 1) + 1;
54
+ const line = source.slice(previousStart, lineEnd).replace(/\r$/, '');
55
+ if (!/^\s*#/.test(line))
56
+ break;
57
+ first = previousStart;
58
+ lineStart = previousStart;
59
+ }
60
+ return first;
61
+ };
62
+ const renderCommentBefore = (comment, indent) => commentLines(comment)
63
+ .map((line) => `${indent}#${line}`)
64
+ .join('\n');
65
+ const commentsOf = (doc) => {
66
+ const comments = [];
67
+ const add = (comment, multiline = false) => {
68
+ if (comment === null || comment === undefined)
69
+ return;
70
+ const values = multiline ? comment.split('\n') : [comment];
71
+ comments.push(...values.map((value) => value.trim()));
72
+ };
73
+ add(doc.comment);
74
+ add(doc.commentBefore, true);
75
+ if (doc.contents) {
76
+ visit(doc.contents, (_key, node) => {
77
+ if (!node || typeof node !== 'object')
78
+ return;
79
+ const commented = node;
80
+ add(commented.comment);
81
+ add(commented.commentBefore, true);
82
+ });
83
+ }
84
+ return comments.sort();
85
+ };
86
+ /** Re-render a sequence in the style and at the indent it was written with. */
87
+ const renderSeq = (seq, entries, source, leadingComment = false) => {
88
+ const texts = entries.map((entry) => entry.text);
89
+ if (seq.flow) {
90
+ const original = seq.range
91
+ ? source.slice(seq.range[0], seq.range[1])
92
+ : '';
93
+ if (!original.includes('\n'))
94
+ return `[${texts.join(', ')}]`;
95
+ const firstItem = seq.items[0];
96
+ if (!isScalar(firstItem) || !firstItem.range) {
97
+ return `[${texts.join(', ')}]`;
98
+ }
99
+ const itemIndent = ' '.repeat(columnOf(source, firstItem.range[0]));
100
+ const closing = original.lastIndexOf(']');
101
+ const closingIndent = ' '.repeat(closing >= 0 ? columnOf(source, seq.range[0] + closing) : columnOf(source, seq.range[0]));
102
+ return `[\n${texts
103
+ .map((text, index) => `${itemIndent}${text}${index === texts.length - 1 ? '' : ','}`)
104
+ .join('\n')}\n${closingIndent}]`;
105
+ }
106
+ const indent = ' '.repeat(columnOf(source, seq.range[0]));
107
+ const rendered = entries.map((entry, index) => {
108
+ const before = renderCommentBefore(entry.commentBefore, indent);
109
+ const itemIndent = index === 0 && !entry.commentBefore && !leadingComment ? '' : indent;
110
+ const item = `${itemIndent}- ${entry.text}${entry.comment ? ` #${entry.comment}` : ''}`;
111
+ return before ? `${before}\n${item}` : item;
112
+ });
113
+ return rendered.join('\n');
114
+ };
115
+ /**
116
+ * Insert `entry` into every test matrix of a workflow, keeping the list sorted.
117
+ * Only the byte range of each matrix sequence is rewritten; the rest of the
118
+ * source passes through untouched, so comments and formatting survive.
119
+ */
120
+ export const addToMatrixYaml = (source, entry) => {
121
+ const doc = parseDocument(source);
122
+ if (doc.errors.length)
123
+ return source;
124
+ const added = {
125
+ value: entry,
126
+ text: stringify(entry).trim()
127
+ };
128
+ const edits = [];
129
+ for (const seq of findMatrixSeqs(doc)) {
130
+ const entries = entriesOf(seq, source);
131
+ if (!entries)
132
+ continue;
133
+ if (entries.some((existing) => existing.value === entry))
134
+ continue;
135
+ if (!seq.range)
136
+ continue;
137
+ if (seq.flow &&
138
+ (entries.some((existing) => existing.comment || existing.commentBefore) ||
139
+ seq.comment ||
140
+ seq.commentBefore)) {
141
+ continue;
142
+ }
143
+ const [originalStart, end] = seq.range;
144
+ const firstCommentStart = !seq.flow && (seq.commentBefore || entries[0]?.commentBefore)
145
+ ? commentBlockStart(source, originalStart)
146
+ : undefined;
147
+ const entriesWithComments = firstCommentStart === undefined || !seq.commentBefore || !entries[0]
148
+ ? entries
149
+ : [
150
+ {
151
+ ...entries[0],
152
+ commentBefore: entries[0].commentBefore
153
+ ? `${seq.commentBefore}\n${entries[0].commentBefore}`
154
+ : seq.commentBefore
155
+ },
156
+ ...entries.slice(1)
157
+ ];
158
+ const next = [...entriesWithComments, added]
159
+ .filter((item, index, all) => all.findIndex((other) => other.value === item.value) === index)
160
+ .sort((left, right) => left.value.localeCompare(right.value));
161
+ const start = firstCommentStart ?? originalStart;
162
+ // a block sequence's range extends to the next token; keep that whitespace
163
+ const [trailing] = /\s*$/.exec(source.slice(originalStart, end));
164
+ const rendered = renderSeq(seq, next, source, firstCommentStart !== undefined);
165
+ const replacement = rendered + trailing;
166
+ const candidate = source.slice(0, start) + replacement + source.slice(end);
167
+ const candidateDoc = parseDocument(candidate);
168
+ if (candidateDoc.errors.length ||
169
+ commentsOf(candidateDoc).join('\0') !== commentsOf(doc).join('\0')) {
170
+ continue;
171
+ }
172
+ edits.push({
173
+ start,
174
+ end,
175
+ text: replacement
176
+ });
177
+ }
178
+ // Last edit first, so earlier ranges keep their offsets.
179
+ return edits
180
+ .sort((left, right) => right.start - left.start)
181
+ .reduce((text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end), source);
182
+ };
183
+ /**
184
+ * Add a module to the test matrix of a workspace's CI workflows, keeping the
185
+ * list sorted. The matrix stays a plain, hand-editable array: workflows without
186
+ * a `jobs.<job>.strategy.matrix.package` sequence are left alone.
187
+ * Best-effort: unreadable or unwritable workflows are skipped, never thrown.
188
+ *
189
+ * Returns the workflow files that changed, relative to `workspacePath`.
190
+ */
191
+ export const addToCiMatrix = (workspacePath, modulePackagePath) => {
192
+ const dir = path.join(workspacePath, WORKFLOW_DIR);
193
+ const entry = modulePackagePath.split(path.sep).join('/');
194
+ const changed = [];
195
+ try {
196
+ if (!fs.existsSync(dir))
197
+ return changed;
198
+ for (const file of fs.readdirSync(dir).sort()) {
199
+ if (!/\.ya?ml$/.test(file))
200
+ continue;
201
+ try {
202
+ const filePath = path.join(dir, file);
203
+ const original = fs.readFileSync(filePath, 'utf8');
204
+ const updated = addToMatrixYaml(original, entry);
205
+ if (updated === original)
206
+ continue;
207
+ fs.writeFileSync(filePath, updated);
208
+ changed.push(path.join(WORKFLOW_DIR, file).split(path.sep).join('/'));
209
+ }
210
+ catch {
211
+ continue;
212
+ }
213
+ }
214
+ }
215
+ catch {
216
+ return changed;
217
+ }
218
+ return changed;
219
+ };
package/esm/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './core/boilerplate-scanner';
2
2
  export * from './core/boilerplate-types';
3
+ export * from './core/ci-matrix';
3
4
  export * from './core/class/pgpm';
4
5
  export * from './core/template-scaffold';
5
6
  export * from './diff';
package/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './core/boilerplate-scanner';
2
2
  export * from './core/boilerplate-types';
3
+ export * from './core/ci-matrix';
3
4
  export * from './core/class/pgpm';
4
5
  export * from './core/template-scaffold';
5
6
  export * from './diff';
package/index.js CHANGED
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.parseAuthor = exports.withTransaction = exports.executeQuery = exports.hashString = exports.hashSqlFile = exports.hashFile = exports.PgpmMigrate = exports.cleanSql = exports.PgpmInit = void 0;
18
18
  __exportStar(require("./core/boilerplate-scanner"), exports);
19
19
  __exportStar(require("./core/boilerplate-types"), exports);
20
+ __exportStar(require("./core/ci-matrix"), exports);
20
21
  __exportStar(require("./core/class/pgpm"), exports);
21
22
  __exportStar(require("./core/template-scaffold"), exports);
22
23
  __exportStar(require("./diff"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpmjs/core",
3
- "version": "7.31.0",
3
+ "version": "7.32.1",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "PGPM Package and Migration Tools",
6
6
  "main": "index.js",
@@ -49,15 +49,15 @@
49
49
  "makage": "^0.8.0"
50
50
  },
51
51
  "dependencies": {
52
- "@pgpmjs/ast": "^0.15.0",
53
- "@pgpmjs/bundle": "^0.18.0",
54
- "@pgpmjs/diff": "^0.8.0",
55
- "@pgpmjs/env": "^2.45.0",
52
+ "@pgpmjs/ast": "^0.15.2",
53
+ "@pgpmjs/bundle": "^0.18.2",
54
+ "@pgpmjs/diff": "^0.8.2",
55
+ "@pgpmjs/env": "^2.45.2",
56
56
  "@pgpmjs/logger": "^2.26.0",
57
- "@pgpmjs/server-utils": "^3.29.0",
58
- "@pgpmjs/slice": "^0.19.0",
59
- "@pgpmjs/transform": "^0.28.0",
60
- "@pgpmjs/types": "^2.55.0",
57
+ "@pgpmjs/server-utils": "^3.29.2",
58
+ "@pgpmjs/slice": "^0.19.2",
59
+ "@pgpmjs/transform": "^0.28.2",
60
+ "@pgpmjs/types": "^2.55.2",
61
61
  "cross-spawn": "^7.0.6",
62
62
  "csv-to-pg": "^4.14.0",
63
63
  "genomic": "^5.6.6",
@@ -65,11 +65,12 @@
65
65
  "glob": "^13.0.6",
66
66
  "parse-package-name": "^1.0.0",
67
67
  "pg": "^8.21.0",
68
- "pg-cache": "^3.29.0",
68
+ "pg-cache": "^3.29.2",
69
69
  "pg-env": "^1.32.0",
70
70
  "pgsql-deparser": "^18.3.7",
71
71
  "pgsql-parser": "^18.2.7",
72
+ "yaml": "^2.9.0",
72
73
  "yanse": "^0.2.2"
73
74
  },
74
- "gitHead": "75b888926505b473eeef7a76fea1554e5a1cf4cc"
75
+ "gitHead": "e05686fb94c5e22c954436f18c25aa4439b9e83e"
75
76
  }