@itcase/config 1.7.25 → 1.7.27
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itcase/config",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.27",
|
|
4
4
|
"author": "ITCase",
|
|
5
5
|
"description": "ITCase Config",
|
|
6
6
|
"engines": {
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"@commitlint/config-conventional": "^21.2.3",
|
|
44
44
|
"@commitlint/cz-commitlint": "^21.2.3",
|
|
45
45
|
"@lehoczky/postcss-fluid": "^1.0.3",
|
|
46
|
-
"@lerna-lite/cli": "^5.6.
|
|
47
|
-
"@lerna-lite/exec": "^5.6.
|
|
48
|
-
"@lerna-lite/list": "^5.6.
|
|
49
|
-
"@lerna-lite/run": "^5.6.
|
|
50
|
-
"@lerna-lite/watch": "^5.6.
|
|
46
|
+
"@lerna-lite/cli": "^5.6.3",
|
|
47
|
+
"@lerna-lite/exec": "^5.6.3",
|
|
48
|
+
"@lerna-lite/list": "^5.6.3",
|
|
49
|
+
"@lerna-lite/run": "^5.6.3",
|
|
50
|
+
"@lerna-lite/watch": "^5.6.3",
|
|
51
51
|
"@rollup/plugin-alias": "^6.0.0",
|
|
52
52
|
"@rollup/plugin-babel": "^7.1.0",
|
|
53
53
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
@@ -68,11 +68,11 @@
|
|
|
68
68
|
"commitizen": "^4.3.2",
|
|
69
69
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
70
70
|
"conventional-changelog-writer": "9.3.0",
|
|
71
|
-
"cssnano": "^9.
|
|
72
|
-
"cssnano-preset-default": "^9.
|
|
71
|
+
"cssnano": "^9.9.0",
|
|
72
|
+
"cssnano-preset-default": "^9.8.0",
|
|
73
73
|
"inquirer": "^14.2.3",
|
|
74
74
|
"log-update": "^8.0.0",
|
|
75
|
-
"postcss": "^8.5.
|
|
75
|
+
"postcss": "^8.5.30",
|
|
76
76
|
"postcss-aspect-ratio-polyfill": "^2.0.0",
|
|
77
77
|
"postcss-clamp": "^4.1.0",
|
|
78
78
|
"postcss-cli": "^12.0.0",
|
|
@@ -80,19 +80,19 @@
|
|
|
80
80
|
"postcss-combine-duplicated-selectors": "^11.0.0",
|
|
81
81
|
"postcss-convert-unit": "^0.1.3",
|
|
82
82
|
"postcss-dark-theme-class": "^2.0.0",
|
|
83
|
-
"postcss-discard-duplicates": "^9.0.
|
|
83
|
+
"postcss-discard-duplicates": "^9.0.7",
|
|
84
84
|
"postcss-each": "^1.1.0",
|
|
85
85
|
"postcss-easings": "^4.0.0",
|
|
86
86
|
"postcss-extend-rule": "^4.0.0",
|
|
87
87
|
"postcss-flexbugs-fixes": "^5.0.2",
|
|
88
88
|
"postcss-hexrgba": "^2.1.0",
|
|
89
|
-
"postcss-import": "^17.0.
|
|
89
|
+
"postcss-import": "^17.0.1",
|
|
90
90
|
"postcss-import-ext-glob": "^2.1.1",
|
|
91
91
|
"postcss-loader": "^8.2.1",
|
|
92
92
|
"postcss-merge-at-rules": "^2.0.1",
|
|
93
93
|
"postcss-mixins": "^12.1.2",
|
|
94
94
|
"postcss-mq-extract": "^1.0.0",
|
|
95
|
-
"postcss-nested": "^
|
|
95
|
+
"postcss-nested": "^9.0.0",
|
|
96
96
|
"postcss-nested-ancestors": "^3.0.0",
|
|
97
97
|
"postcss-normalize": "^13.0.1",
|
|
98
98
|
"postcss-prepend-imports": "^1.0.1",
|
|
@@ -110,17 +110,17 @@
|
|
|
110
110
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
111
111
|
"rollup-preserve-directives": "^1.1.3",
|
|
112
112
|
"semantic-release": "^25.0.9",
|
|
113
|
-
"semantic-release-lerna": "^3.6.
|
|
113
|
+
"semantic-release-lerna": "^3.6.7"
|
|
114
114
|
},
|
|
115
115
|
"devDependencies": {
|
|
116
116
|
"@itcase/agent-skills": "^1.0.13",
|
|
117
|
-
"@itcase/lint": "^1.1.
|
|
117
|
+
"@itcase/lint": "^1.1.130",
|
|
118
118
|
"eslint": "9.39.2",
|
|
119
119
|
"husky": "^9.1.7",
|
|
120
120
|
"inquirer": "^14.2.3",
|
|
121
121
|
"lint-staged": "^17.6.0",
|
|
122
|
-
"prettier": "^3.9.
|
|
123
|
-
"storybook": "^10.6.
|
|
122
|
+
"prettier": "^3.9.10",
|
|
123
|
+
"storybook": "^10.6.2",
|
|
124
124
|
"stylelint": "^17.16.0",
|
|
125
125
|
"typescript": "^6.0.3"
|
|
126
126
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const MAILRU_WEBDAV_BASE = 'https://webdav.cloud.mail.ru'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prepare: bump Android → make APK → Mail.ru (если GIT_WORKFLOW=true).
|
|
9
|
+
*
|
|
10
|
+
* Env: SKIP_APK_BUILD, RELEASE_APK_TARGETS (stage,prod),
|
|
11
|
+
* MAILRU_USER, MAILRU_APP_PASSWORD, MAILRU_PUBLIC_URL.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ apkPrefix: string, mailruDir: string }} options
|
|
14
|
+
*/
|
|
15
|
+
function createAndroidAppPreparePlugin({ apkPrefix, mailruDir }) {
|
|
16
|
+
if (!apkPrefix) {
|
|
17
|
+
throw new Error('createAndroidAppPreparePlugin: apkPrefix is required')
|
|
18
|
+
}
|
|
19
|
+
if (!mailruDir) {
|
|
20
|
+
throw new Error('createAndroidAppPreparePlugin: mailruDir is required')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const uploadDir = mailruDir.replace(/^\/+|\/+$/g, '')
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
prepare: async (_pluginConfig, context) => {
|
|
27
|
+
const { logger, nextRelease, options } = context
|
|
28
|
+
const version = nextRelease.version
|
|
29
|
+
|
|
30
|
+
bumpAndroidVersion(version, logger)
|
|
31
|
+
|
|
32
|
+
if (options.dryRun || process.env.SKIP_APK_BUILD === '1') {
|
|
33
|
+
logger.log('Skip APK build (dry-run or SKIP_APK_BUILD=1)')
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const apks = resolveApks(apkPrefix, version)
|
|
38
|
+
for (const apk of apks) {
|
|
39
|
+
logger.log(`make ${apk.make}`)
|
|
40
|
+
execFileSync('make', [apk.make], { stdio: 'inherit' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
uploadApks(apks, uploadDir, logger)
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bumpAndroidVersion(version, logger) {
|
|
49
|
+
if (!/^\d+\.\d+\.\d+(-[\w.-]+)?$/.test(version)) {
|
|
50
|
+
throw new Error(`Unsupported version format: "${version}"`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const gradlePath = join(process.cwd(), 'android/app/build.gradle')
|
|
54
|
+
const content = readFileSync(gradlePath, 'utf8')
|
|
55
|
+
const vNameMatch = content.match(/def vName = "([^"]+)"/)
|
|
56
|
+
const vCodeMatch = content.match(/def vCode = (\d+)/)
|
|
57
|
+
|
|
58
|
+
if (!vNameMatch || !vCodeMatch) {
|
|
59
|
+
throw new Error('Could not find version in build.gradle')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (vNameMatch[1] === version) {
|
|
63
|
+
logger.log(`Android vName already ${version}, skip bump`)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const newCode = Number(vCodeMatch[1]) + 1
|
|
68
|
+
writeFileSync(
|
|
69
|
+
gradlePath,
|
|
70
|
+
content
|
|
71
|
+
.replace(/def vName = "[^"]+"/, `def vName = "${version}"`)
|
|
72
|
+
.replace(/def vCode = \d+/, `def vCode = ${newCode}`),
|
|
73
|
+
)
|
|
74
|
+
logger.log(`Android vName = ${version}, vCode = ${newCode}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @returns {{ make: string, name: string, fileName: string }[]} */
|
|
78
|
+
function resolveApks(apkPrefix, version) {
|
|
79
|
+
const defs = {
|
|
80
|
+
prod: {
|
|
81
|
+
name: 'PROD',
|
|
82
|
+
fileName: `${apkPrefix}-v${version}.apk`,
|
|
83
|
+
make: 'android-apk-prod',
|
|
84
|
+
},
|
|
85
|
+
stage: {
|
|
86
|
+
name: 'STAGE',
|
|
87
|
+
fileName: `${apkPrefix}-v${version}-STAGE.apk`,
|
|
88
|
+
make: 'android-apk-stage',
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const keys = (process.env.RELEASE_APK_TARGETS || 'stage,prod')
|
|
93
|
+
.split(',')
|
|
94
|
+
.map((part) => part.trim())
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
|
|
97
|
+
if (keys.length === 0) {
|
|
98
|
+
throw new Error('RELEASE_APK_TARGETS is empty')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return keys.map((key) => {
|
|
102
|
+
const apk = defs[key]
|
|
103
|
+
if (!apk) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Unknown RELEASE_APK_TARGETS entry: "${key}". Use stage, prod`,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
return apk
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function uploadApks(apks, uploadDir, logger) {
|
|
113
|
+
if (process.env.GIT_WORKFLOW !== 'true') {
|
|
114
|
+
logger.log('Skip Mail.ru upload (not CI)')
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const user = process.env.MAILRU_USER
|
|
119
|
+
const password = process.env.MAILRU_APP_PASSWORD
|
|
120
|
+
const publicUrl = (process.env.MAILRU_PUBLIC_URL || '').replace(/\/$/, '')
|
|
121
|
+
|
|
122
|
+
if (!user || !password || !publicUrl) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
'MAILRU_USER, MAILRU_APP_PASSWORD and MAILRU_PUBLIC_URL are required',
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const uploadBase = `${MAILRU_WEBDAV_BASE}/${uploadDir}`
|
|
129
|
+
const downloads = []
|
|
130
|
+
|
|
131
|
+
for (const apk of apks) {
|
|
132
|
+
logger.log(`Upload ${apk.fileName} to ${uploadBase}/`)
|
|
133
|
+
execFileSync(
|
|
134
|
+
'curl',
|
|
135
|
+
[
|
|
136
|
+
'--fail',
|
|
137
|
+
'--silent',
|
|
138
|
+
'--show-error',
|
|
139
|
+
'--retry',
|
|
140
|
+
'2',
|
|
141
|
+
'--retry-delay',
|
|
142
|
+
'2',
|
|
143
|
+
'-T',
|
|
144
|
+
join(process.cwd(), 'builds', apk.fileName),
|
|
145
|
+
'--user',
|
|
146
|
+
`${user}:${password}`,
|
|
147
|
+
`${uploadBase}/${apk.fileName}`,
|
|
148
|
+
],
|
|
149
|
+
{ stdio: 'inherit' },
|
|
150
|
+
)
|
|
151
|
+
downloads.push({ name: apk.name, url: `${publicUrl}/${apk.fileName}` })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
writeFileSync(
|
|
155
|
+
join(process.cwd(), 'builds', 'mailru-downloads.json'),
|
|
156
|
+
JSON.stringify(downloads),
|
|
157
|
+
)
|
|
158
|
+
logger.log('APK upload finished — Mail.ru links saved')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { createAndroidAppPreparePlugin }
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createSemanticReleaseApp,
|
|
3
|
-
semanticReleaseApp,
|
|
4
|
-
} from './semanticReleaseApp.js'
|
|
1
|
+
import { semanticReleaseApp } from './semanticReleaseApp.js'
|
|
5
2
|
import { semanticReleaseBot } from './semanticReleaseBot.js'
|
|
6
3
|
import { semanticReleaseDoc } from './semanticReleaseDoc.js'
|
|
7
4
|
import { semanticReleaseMonorepo } from './semanticReleaseMonorepo.js'
|
|
@@ -10,7 +7,6 @@ import { semanticReleaseProject } from './semanticReleaseProject.js'
|
|
|
10
7
|
import { semanticReleaseVSMarketplace } from './semanticReleaseVSMarketplace.js'
|
|
11
8
|
|
|
12
9
|
export {
|
|
13
|
-
createSemanticReleaseApp,
|
|
14
10
|
semanticReleaseApp,
|
|
15
11
|
semanticReleaseBot,
|
|
16
12
|
semanticReleaseDoc,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAndroidAppPreparePlugin } from './androidAppPrepare.js'
|
|
1
2
|
import {
|
|
2
3
|
createSemanticReleaseConfig,
|
|
3
4
|
releaseNotesFromIssuesPlugin,
|
|
@@ -17,27 +18,44 @@ const DEFAULT_RELEASE_ASSETS = ['builds/*.apk']
|
|
|
17
18
|
*
|
|
18
19
|
* - Не публикует в npm
|
|
19
20
|
* - Коммитит package + Android version-файлы
|
|
20
|
-
* - Прикрепляет к GitHub release APK из `builds/`
|
|
21
|
-
* - Заметки релиза: сначала
|
|
21
|
+
* - Прикрепляет к GitHub release APK из `builds/`
|
|
22
|
+
* - Заметки релиза: сначала задачи, потом коммиты
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
+
* Если передан `apk`, в prepare: bump → make → Mail.ru (GIT_WORKFLOW=true).
|
|
24
25
|
*
|
|
25
26
|
* @param {Object} [options]
|
|
26
27
|
* @param {string|string[]} [options.branches=['master']]
|
|
27
|
-
* @param {
|
|
28
|
-
* @param {string
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {
|
|
31
|
-
* @param {Object} [options.
|
|
28
|
+
* @param {Object} [options.apk]
|
|
29
|
+
* @param {string} options.apk.prefix - agro-pps → builds/agro-pps-v*.apk
|
|
30
|
+
* @param {string} options.apk.mailruDir - папка WebDAV, напр. APK
|
|
31
|
+
* @param {string[]} [options.versionAssets]
|
|
32
|
+
* @param {string[]|Object[]} [options.releaseAssets] - по умолчанию builds/{prefix}-v*.apk
|
|
33
|
+
* @param {Array} [options.preparePlugins]
|
|
34
|
+
* @param {Object} [options.githubConfig]
|
|
35
|
+
* @param {Object} [options.gitConfig]
|
|
32
36
|
*/
|
|
33
|
-
function
|
|
37
|
+
function semanticReleaseApp({
|
|
38
|
+
apk,
|
|
34
39
|
branches = ['master'],
|
|
35
40
|
gitConfig = {},
|
|
36
41
|
githubConfig = {},
|
|
37
42
|
preparePlugins = [],
|
|
38
|
-
releaseAssets
|
|
43
|
+
releaseAssets,
|
|
39
44
|
versionAssets = DEFAULT_VERSION_ASSETS,
|
|
40
45
|
} = {}) {
|
|
46
|
+
const androidPrepare = apk
|
|
47
|
+
? [
|
|
48
|
+
createAndroidAppPreparePlugin({
|
|
49
|
+
apkPrefix: apk.prefix,
|
|
50
|
+
mailruDir: apk.mailruDir,
|
|
51
|
+
}),
|
|
52
|
+
]
|
|
53
|
+
: []
|
|
54
|
+
|
|
55
|
+
const githubAssets =
|
|
56
|
+
releaseAssets ??
|
|
57
|
+
(apk?.prefix ? [`builds/${apk.prefix}-v*.apk`] : DEFAULT_RELEASE_ASSETS)
|
|
58
|
+
|
|
41
59
|
return createSemanticReleaseConfig({
|
|
42
60
|
additionalPlugins: [releaseNotesFromIssuesPlugin],
|
|
43
61
|
branches: branches,
|
|
@@ -48,16 +66,14 @@ function createSemanticReleaseApp({
|
|
|
48
66
|
...gitConfig,
|
|
49
67
|
},
|
|
50
68
|
githubConfig: {
|
|
51
|
-
assets:
|
|
69
|
+
assets: githubAssets,
|
|
52
70
|
...githubConfig,
|
|
53
71
|
},
|
|
54
72
|
npmConfig: {
|
|
55
73
|
npmPublish: false,
|
|
56
74
|
},
|
|
57
|
-
preparePlugins: preparePlugins,
|
|
75
|
+
preparePlugins: [...androidPrepare, ...preparePlugins],
|
|
58
76
|
})
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
export { createSemanticReleaseApp, semanticReleaseApp }
|
|
79
|
+
export { semanticReleaseApp }
|
package/smart-commit/cli.js
CHANGED
|
@@ -24,10 +24,18 @@ const CLI_RELATIVE = 'smart-commit/cli.js'
|
|
|
24
24
|
// Cheap Cursor Models pool — commit messages do not need Auto / frontier models.
|
|
25
25
|
const DEFAULT_MODEL = 'composer-2.5'
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// A small change set still sends the patch. A large tree used to be many
|
|
28
|
+
// agent calls with up to 80k chars of diff each. Compact numstat keeps that
|
|
29
|
+
// to one call.
|
|
30
|
+
const MAX_DIFF = 20_000
|
|
31
|
+
const COMPACT_SUMMARY_MAX = 12_000
|
|
32
|
+
const SMALL_PLAN_FILES = 12
|
|
33
|
+
const PER_FILE_EXCERPT = 500
|
|
34
|
+
const EXCERPT_SKIP_LINES = 400
|
|
35
|
+
const EXCERPT_MAX_BYTES = 256 * 1024
|
|
28
36
|
const GIT_MAX_BUFFER = 32 * 1024 * 1024
|
|
29
37
|
const GIT_PATH_BATCH = 40
|
|
30
|
-
const PLAN_FILE_BATCH =
|
|
38
|
+
const PLAN_FILE_BATCH = 250
|
|
31
39
|
const ARG_SAFE = 100_000
|
|
32
40
|
const LOCKFILE_NAMES = new Set([
|
|
33
41
|
'package-lock.json',
|
|
@@ -608,8 +616,10 @@ function buildPrompt(scopes, types, files, cwd, type) {
|
|
|
608
616
|
|
|
609
617
|
const sourceFiles = files.filter((file) => !isLockfile(file))
|
|
610
618
|
const lockFiles = files.filter((file) => isLockfile(file))
|
|
619
|
+
const diffLimit =
|
|
620
|
+
sourceFiles.length > SMALL_PLAN_FILES ? COMPACT_SUMMARY_MAX : MAX_DIFF
|
|
611
621
|
let diff = buildSourceDiff(cwd, sourceFiles)
|
|
612
|
-
if (diff.length >
|
|
622
|
+
if (diff.length > diffLimit) diff = `${diff.slice(0, diffLimit)}\n…truncated`
|
|
613
623
|
|
|
614
624
|
return `Plan git commits. Do not edit files or run git commit.
|
|
615
625
|
Return ONLY JSON: {"commits":[{"message":"type(scope): subject","files":["a.ts"]}]}
|
|
@@ -622,6 +632,7 @@ Prefer one commit when changes belong to the same feature.
|
|
|
622
632
|
Split by scope. Each listed file in exactly one commit — including deleted files (D) and new/untracked files (??).
|
|
623
633
|
Put each lockfile in the same commit as its sibling package.json. Do not make a lockfile-only commit.
|
|
624
634
|
Do not add Co-authored-by or Made-with trailers.
|
|
635
|
+
Do not use tools or read files. The file list and diff below are the only source.
|
|
625
636
|
${type ? `Prefer type "${type}".\n` : ''}Files:
|
|
626
637
|
${files
|
|
627
638
|
.map((file) => {
|
|
@@ -644,6 +655,11 @@ ${diff || '(none)'}
|
|
|
644
655
|
}
|
|
645
656
|
|
|
646
657
|
function buildSourceDiff(cwd, files) {
|
|
658
|
+
if (files.length > SMALL_PLAN_FILES) return buildCompactSourceDiff(cwd, files)
|
|
659
|
+
return buildFullSourceDiff(cwd, files, MAX_DIFF)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function buildFullSourceDiff(cwd, files, limit) {
|
|
647
663
|
const tracked = []
|
|
648
664
|
const untracked = []
|
|
649
665
|
for (const file of files) {
|
|
@@ -654,13 +670,13 @@ function buildSourceDiff(cwd, files) {
|
|
|
654
670
|
const parts = []
|
|
655
671
|
let used = 0
|
|
656
672
|
const pushLimited = (text) => {
|
|
657
|
-
if (used >=
|
|
673
|
+
if (used >= limit) return false
|
|
658
674
|
// Empty batch (no changes / ENOBUFS skip) — keep scanning remaining files.
|
|
659
675
|
if (!text) return true
|
|
660
|
-
const room =
|
|
676
|
+
const room = limit - used
|
|
661
677
|
if (text.length > room) {
|
|
662
678
|
parts.push(`${text.slice(0, room)}\n…truncated`)
|
|
663
|
-
used =
|
|
679
|
+
used = limit
|
|
664
680
|
return false
|
|
665
681
|
}
|
|
666
682
|
parts.push(text)
|
|
@@ -669,17 +685,120 @@ function buildSourceDiff(cwd, files) {
|
|
|
669
685
|
}
|
|
670
686
|
|
|
671
687
|
forEachBatch(tracked, (batch) => {
|
|
672
|
-
if (used >=
|
|
688
|
+
if (used >= limit) return false
|
|
673
689
|
return pushLimited(tryOut(cwd, ['diff', 'HEAD', '--', ...batch]))
|
|
674
690
|
})
|
|
675
691
|
for (const file of untracked) {
|
|
676
|
-
if (used >=
|
|
692
|
+
if (used >= limit) break
|
|
677
693
|
const piece = buildUntrackedDiff(cwd, file)
|
|
678
694
|
if (!pushLimited(piece)) break
|
|
679
695
|
}
|
|
680
696
|
return parts.join('\n\n')
|
|
681
697
|
}
|
|
682
698
|
|
|
699
|
+
function buildCompactSourceDiff(cwd, files) {
|
|
700
|
+
const tracked = []
|
|
701
|
+
const untracked = new Set()
|
|
702
|
+
for (const file of files) {
|
|
703
|
+
if (isUntracked(cwd, file)) untracked.add(file)
|
|
704
|
+
else tracked.push(file)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const statLines = []
|
|
708
|
+
forEachBatch(tracked, (batch) => {
|
|
709
|
+
const text = tryOut(cwd, ['diff', '--numstat', 'HEAD', '--', ...batch])
|
|
710
|
+
if (text) statLines.push(...text.split('\n').filter(Boolean))
|
|
711
|
+
})
|
|
712
|
+
for (const file of untracked) statLines.push(untrackedNumstatLine(cwd, file))
|
|
713
|
+
|
|
714
|
+
const weightByFile = new Map()
|
|
715
|
+
for (const line of statLines) {
|
|
716
|
+
const file = line.split('\t').slice(2).join('\t')
|
|
717
|
+
weightByFile.set(file, parseNumstatWeight(line))
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const header = `numstat (added, removed, path):\n${statLines.join('\n')}`
|
|
721
|
+
if (header.length >= COMPACT_SUMMARY_MAX) {
|
|
722
|
+
return `${header.slice(0, COMPACT_SUMMARY_MAX)}\n…truncated`
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const ranked = [...files].sort((left, right) => {
|
|
726
|
+
const leftWeight = weightByFile.get(left)
|
|
727
|
+
const rightWeight = weightByFile.get(right)
|
|
728
|
+
return (rightWeight ?? -1) - (leftWeight ?? -1)
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
const excerptHeader = '\n\nchanged lines:\n'
|
|
732
|
+
let room = COMPACT_SUMMARY_MAX - header.length - excerptHeader.length
|
|
733
|
+
const excerpts = []
|
|
734
|
+
for (const file of ranked) {
|
|
735
|
+
if (room < 40) break
|
|
736
|
+
const weight = weightByFile.get(file)
|
|
737
|
+
if (weight == null || weight > EXCERPT_SKIP_LINES) continue
|
|
738
|
+
if (weight === 0 && !untracked.has(file)) continue
|
|
739
|
+
const excerpt = changedLineExcerpt(
|
|
740
|
+
cwd,
|
|
741
|
+
file,
|
|
742
|
+
untracked.has(file),
|
|
743
|
+
Math.min(PER_FILE_EXCERPT, room),
|
|
744
|
+
)
|
|
745
|
+
if (!excerpt) continue
|
|
746
|
+
excerpts.push(excerpt)
|
|
747
|
+
room -= excerpt.length + 1
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (!excerpts.length) return header
|
|
751
|
+
return `${header}${excerptHeader}${excerpts.join('\n')}`
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function parseNumstatWeight(line) {
|
|
755
|
+
const [added, removed] = line.split('\t')
|
|
756
|
+
if (added === '-' || removed === '-') return null
|
|
757
|
+
return (Number(added) || 0) + (Number(removed) || 0)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function untrackedNumstatLine(cwd, file) {
|
|
761
|
+
const text = readTextFileHead(cwd, file)
|
|
762
|
+
if (text == null) return `-\t-\t${file}`
|
|
763
|
+
const lines = text ? text.split('\n').length : 0
|
|
764
|
+
return `${lines}\t0\t${file}`
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function readTextFileHead(cwd, file) {
|
|
768
|
+
const filePath = path.join(cwd, file)
|
|
769
|
+
let stat
|
|
770
|
+
try {
|
|
771
|
+
stat = fs.statSync(filePath)
|
|
772
|
+
} catch {
|
|
773
|
+
return null
|
|
774
|
+
}
|
|
775
|
+
if (!stat.isFile() || stat.size > EXCERPT_MAX_BYTES) return null
|
|
776
|
+
const buffer = fs.readFileSync(filePath)
|
|
777
|
+
if (buffer.includes(0)) return null
|
|
778
|
+
return buffer.toString('utf8')
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function changedLineExcerpt(cwd, file, untracked, cap) {
|
|
782
|
+
if (untracked) {
|
|
783
|
+
const text = readTextFileHead(cwd, file)
|
|
784
|
+
if (!text) return ''
|
|
785
|
+
const body = text.length > cap ? `${text.slice(0, cap)}…` : text
|
|
786
|
+
return `${file}:\n${body}`
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const raw = tryOut(cwd, ['diff', '-U0', '--no-color', 'HEAD', '--', file])
|
|
790
|
+
if (!raw) return ''
|
|
791
|
+
const lines = []
|
|
792
|
+
for (const line of raw.split('\n')) {
|
|
793
|
+
if (line.startsWith('+++') || line.startsWith('---')) continue
|
|
794
|
+
if (line.startsWith('+') || line.startsWith('-')) lines.push(line)
|
|
795
|
+
}
|
|
796
|
+
if (!lines.length) return ''
|
|
797
|
+
let body = lines.join('\n')
|
|
798
|
+
if (body.length > cap) body = `${body.slice(0, cap)}…`
|
|
799
|
+
return `${file}:\n${body}`
|
|
800
|
+
}
|
|
801
|
+
|
|
683
802
|
function buildUntrackedDiff(cwd, file) {
|
|
684
803
|
const filePath = path.join(cwd, file)
|
|
685
804
|
if (!fs.existsSync(filePath)) return ''
|