git-stack-cli 1.12.0 → 1.13.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
 
3
- import fs from "node:fs";
3
+ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
6
  import * as Ink from "ink-cjs";
@@ -90,7 +90,8 @@ export function AutoUpdate(props: Props) {
90
90
  throw new Error("Unable to retrieve latest version from npm");
91
91
  }
92
92
 
93
- const script_dir = path.dirname(fs.realpathSync(process.argv[1]));
93
+ const script_path = await fs.realpath(process.argv[1]);
94
+ const script_dir = path.dirname(script_path);
94
95
 
95
96
  // dist/ts/index.js
96
97
  const package_json_path = path.join(
@@ -100,7 +101,8 @@ export function AutoUpdate(props: Props) {
100
101
  "package.json"
101
102
  );
102
103
 
103
- const package_json = read_json<{ version: string }>(package_json_path);
104
+ type PackageJson = { version: string };
105
+ const package_json = await read_json<PackageJson>(package_json_path);
104
106
 
105
107
  if (!package_json) {
106
108
  // unable to find read package.json, skip auto update
@@ -1,6 +1,5 @@
1
1
  import * as React from "react";
2
2
 
3
- import fs from "node:fs";
4
3
  import path from "node:path";
5
4
 
6
5
  import * as Ink from "ink-cjs";
@@ -11,6 +10,7 @@ import { Store } from "~/app/Store";
11
10
  import { YesNoPrompt } from "~/app/YesNoPrompt";
12
11
  import { cli } from "~/core/cli";
13
12
  import { colors } from "~/core/colors";
13
+ import { safe_exists } from "~/core/safe_exists";
14
14
 
15
15
  type Props = {
16
16
  children: React.ReactNode;
@@ -73,11 +73,11 @@ export function CherryPickCheck(props: Props) {
73
73
  try {
74
74
  const git_dir = (await cli(`git rev-parse --absolute-git-dir`)).stdout;
75
75
 
76
- const is_cherry_pick = fs.existsSync(
77
- path.join(git_dir, "CHERRY_PICK_HEAD")
78
- );
76
+ const cherry_pick_file = path.join(git_dir, "CHERRY_PICK_HEAD");
77
+ const is_cherry_pick = await safe_exists(cherry_pick_file);
79
78
 
80
79
  const status = is_cherry_pick ? "prompt" : "done";
80
+
81
81
  patch({ status });
82
82
  } catch (err) {
83
83
  actions.error("Must be run from within a git repository.");
package/src/app/Debug.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
 
3
- import fs from "node:fs";
3
+ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
6
  import * as Ink from "ink-cjs";
@@ -8,6 +8,7 @@ import * as Ink from "ink-cjs";
8
8
  import { Store } from "~/app/Store";
9
9
  import { colors } from "~/core/colors";
10
10
  import * as json from "~/core/json";
11
+ import { safe_rm } from "~/core/safe_rm";
11
12
 
12
13
  export function Debug() {
13
14
  const actions = Store.useActions();
@@ -27,20 +28,22 @@ export function Debug() {
27
28
  );
28
29
 
29
30
  React.useEffect(
30
- function syncStateJson() {
31
+ function sync_state_json() {
31
32
  if (!argv?.["write-state-json"]) {
32
33
  return;
33
34
  }
34
35
 
35
- const output_file = path.join(state.cwd, "git-stack-state.json");
36
+ sync().catch(actions.error);
36
37
 
37
- if (fs.existsSync(output_file)) {
38
- fs.rmSync(output_file);
39
- }
38
+ async function sync() {
39
+ const output_file = path.join(state.cwd, "git-stack-state.json");
40
+
41
+ await safe_rm(output_file);
40
42
 
41
- const serialized = json.serialize(state);
42
- const content = JSON.stringify(serialized, null, 2);
43
- fs.writeFileSync(output_file, content);
43
+ const serialized = json.serialize(state);
44
+ const content = JSON.stringify(serialized, null, 2);
45
+ await fs.writeFile(output_file, content);
46
+ }
44
47
  },
45
48
  [argv, state]
46
49
  );
@@ -163,12 +163,6 @@ function CheckGithubCliAuth(props: Props) {
163
163
 
164
164
  function CheckGitRevise(props: Props) {
165
165
  const actions = Store.useActions();
166
- const argv = Store.useState((state) => state.argv);
167
-
168
- // skip git revise check when `rebase` is not git-revise
169
- if (argv?.["rebase"] !== "git-revise") {
170
- return props.children;
171
- }
172
166
 
173
167
  return (
174
168
  <Await
package/src/app/Main.tsx CHANGED
@@ -10,6 +10,7 @@ import { PreSelectCommitRanges } from "~/app/PreSelectCommitRanges";
10
10
  import { SelectCommitRanges } from "~/app/SelectCommitRanges";
11
11
  import { Status } from "~/app/Status";
12
12
  import { Store } from "~/app/Store";
13
+ import { SyncGithub } from "~/app/SyncGithub";
13
14
  import { assertNever } from "~/core/assertNever";
14
15
 
15
16
  export function Main() {
@@ -43,6 +44,9 @@ export function Main() {
43
44
  case "manual-rebase":
44
45
  return <ManualRebase />;
45
46
 
47
+ case "sync-github":
48
+ return <SyncGithub />;
49
+
46
50
  case "post-rebase-status":
47
51
  return <PostRebaseStatus />;
48
52
 
@@ -6,28 +6,44 @@ import * as Ink from "ink-cjs";
6
6
 
7
7
  import { Await } from "~/app/Await";
8
8
  import { Brackets } from "~/app/Brackets";
9
- import { FormatText } from "~/app/FormatText";
10
9
  import { Store } from "~/app/Store";
11
10
  import * as CommitMetadata from "~/core/CommitMetadata";
12
11
  import { GitReviseTodo } from "~/core/GitReviseTodo";
13
- import * as Metadata from "~/core/Metadata";
14
- import * as StackSummaryTable from "~/core/StackSummaryTable";
15
12
  import { cli } from "~/core/cli";
16
13
  import { colors } from "~/core/colors";
17
- import * as github from "~/core/github";
18
14
  import { invariant } from "~/core/invariant";
19
15
  import { short_id } from "~/core/short_id";
20
16
 
21
17
  export function ManualRebase() {
18
+ const abort_handler = React.useRef(() => {});
19
+
20
+ React.useEffect(function listen_sigint() {
21
+ process.once("SIGINT", sigint_handler);
22
+
23
+ return function cleanup() {
24
+ process.removeListener("SIGINT", sigint_handler);
25
+ };
26
+
27
+ async function sigint_handler() {
28
+ abort_handler.current();
29
+ }
30
+ }, []);
31
+
22
32
  return (
23
33
  <Await
24
34
  fallback={<Ink.Text color={colors.yellow}>Rebasing commits…</Ink.Text>}
25
- function={run}
35
+ function={async function () {
36
+ await run({ abort_handler });
37
+ }}
26
38
  />
27
39
  );
28
40
  }
29
41
 
30
- async function run() {
42
+ type Args = {
43
+ abort_handler: React.MutableRefObject<() => void>;
44
+ };
45
+
46
+ async function run(args: Args) {
31
47
  const state = Store.getState();
32
48
  const actions = state.actions;
33
49
  const argv = state.argv;
@@ -42,108 +58,70 @@ async function run() {
42
58
  invariant(repo_root, "repo_root must exist");
43
59
 
44
60
  // always listen for SIGINT event and restore git state
45
- process.once("SIGINT", handle_exit);
61
+ args.abort_handler.current = function sigint_handler() {
62
+ actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
63
+ handle_exit(15);
64
+ };
46
65
 
47
- // get latest merge_base relative to local master
48
- const merge_base = (await cli(`git merge-base HEAD ${master_branch}`)).stdout;
66
+ const temp_branch_name = `${branch_name}_${short_id()}`;
49
67
 
50
- // immediately paint all commit to preserve selected commit ranges
51
- let commit_range = await CommitMetadata.range(commit_map);
68
+ try {
69
+ // get latest merge_base relative to local master
70
+ const merge_base = (await cli(`git merge-base HEAD ${master_branch}`))
71
+ .stdout;
52
72
 
53
- // reverse group list to ensure we create git revise in correct order
54
- commit_range.group_list.reverse();
73
+ // immediately paint all commit to preserve selected commit ranges
74
+ let commit_range = await CommitMetadata.range(commit_map);
55
75
 
56
- for (const commit of commit_range.commit_list) {
57
- const group_from_map = commit_map[commit.sha];
58
- commit.branch_id = group_from_map.id;
59
- commit.title = group_from_map.title;
60
- }
76
+ // reverse group list to ensure we create git revise in correct order
77
+ commit_range.group_list.reverse();
61
78
 
62
- await GitReviseTodo.execute({
63
- rebase_group_index: 0,
64
- rebase_merge_base: merge_base,
65
- commit_range,
66
- });
79
+ for (const commit of commit_range.commit_list) {
80
+ const group_from_map = commit_map[commit.sha];
81
+ commit.branch_id = group_from_map.id;
82
+ commit.title = group_from_map.title;
83
+ }
67
84
 
68
- let DEFAULT_PR_BODY = "";
69
- if (state.pr_template_body) {
70
- DEFAULT_PR_BODY = state.pr_template_body;
71
- }
85
+ await GitReviseTodo.execute({
86
+ rebase_group_index: 0,
87
+ rebase_merge_base: merge_base,
88
+ commit_range,
89
+ });
72
90
 
73
- const temp_branch_name = `${branch_name}_${short_id()}`;
91
+ commit_range = await CommitMetadata.range(commit_map);
74
92
 
75
- commit_range = await CommitMetadata.range(commit_map);
93
+ // reverse commit list so that we can cherry-pick in order
94
+ commit_range.group_list.reverse();
76
95
 
77
- // reverse commit list so that we can cherry-pick in order
78
- commit_range.group_list.reverse();
96
+ let rebase_merge_base = merge_base;
97
+ let rebase_group_index = 0;
79
98
 
80
- let rebase_merge_base = merge_base;
81
- let rebase_group_index = 0;
99
+ for (let i = 0; i < commit_range.group_list.length; i++) {
100
+ const group = commit_range.group_list[i];
82
101
 
83
- for (let i = 0; i < commit_range.group_list.length; i++) {
84
- const group = commit_range.group_list[i];
102
+ if (!group.dirty) {
103
+ continue;
104
+ }
85
105
 
86
- if (!group.dirty) {
87
- continue;
88
- }
106
+ if (i > 0) {
107
+ const prev_group = commit_range.group_list[i - 1];
108
+ const prev_commit = prev_group.commits[prev_group.commits.length - 1];
109
+ rebase_merge_base = prev_commit.sha;
110
+ rebase_group_index = i;
111
+ }
89
112
 
90
- if (i > 0) {
91
- const prev_group = commit_range.group_list[i - 1];
92
- const prev_commit = prev_group.commits[prev_group.commits.length - 1];
93
- rebase_merge_base = prev_commit.sha;
94
- rebase_group_index = i;
113
+ break;
95
114
  }
96
115
 
97
- break;
98
- }
116
+ actions.debug(`rebase_merge_base = ${rebase_merge_base}`);
117
+ actions.debug(`rebase_group_index = ${rebase_group_index}`);
99
118
 
100
- actions.debug(`rebase_merge_base = ${rebase_merge_base}`);
101
- actions.debug(`rebase_group_index = ${rebase_group_index}`);
119
+ // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
102
120
 
103
- // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
104
-
105
- try {
106
121
  // must perform rebase from repo root for applying git patch
107
122
  process.chdir(repo_root);
108
123
  await cli(`pwd`);
109
124
 
110
- if (argv["rebase"] === "git-revise") {
111
- await rebase_git_revise();
112
- } else {
113
- await rebase_cherry_pick();
114
- }
115
-
116
- // after all commits have been cherry-picked and amended
117
- // move the branch pointer to the newly created temporary branch
118
- // now we are in locally in sync with github and on the original branch
119
- await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
120
-
121
- restore_git();
122
-
123
- actions.set((state) => {
124
- state.step = "post-rebase-status";
125
- });
126
- } catch (err) {
127
- actions.error("Unable to rebase.");
128
-
129
- if (err instanceof Error) {
130
- if (actions.isDebug()) {
131
- actions.error(err.message);
132
- }
133
- }
134
-
135
- handle_exit();
136
- }
137
-
138
- async function rebase_git_revise() {
139
- actions.debug(`rebase_git_revise`);
140
-
141
- actions.output(
142
- <Ink.Text color={colors.yellow} wrap="truncate-end">
143
- Rebasing…
144
- </Ink.Text>
145
- );
146
-
147
125
  // create temporary branch
148
126
  await cli(`git checkout -b ${temp_branch_name}`);
149
127
 
@@ -153,268 +131,33 @@ async function run() {
153
131
  commit_range,
154
132
  });
155
133
 
156
- // early return since we do not need to sync
157
- if (!argv.sync) {
158
- return;
159
- }
160
-
161
- // in order to sync we walk from rebase_group_index to HEAD
162
- // checking out each group and syncing to github
163
-
164
- // start from HEAD and work backward to rebase_group_index
165
- const push_group_list = [];
166
- let lookback_index = 0;
167
- for (let i = 0; i < commit_range.group_list.length; i++) {
168
- const index = commit_range.group_list.length - 1 - i;
169
-
170
- // do not go past rebase_group_index
171
- if (index < rebase_group_index) {
172
- break;
173
- }
174
-
175
- const group = commit_range.group_list[index];
176
- // console.debug({ i, index, group });
177
-
178
- if (i > 0) {
179
- const prev_group = commit_range.group_list[index + 1];
180
- lookback_index += prev_group.commits.length;
181
- }
182
-
183
- // console.debug(`git show head~${lookback_index}`);
184
-
185
- // push group and lookback_index onto front of push_group_list
186
- push_group_list.unshift({ group, lookback_index });
187
- }
188
-
189
- const pr_url_list = commit_range.group_list.map(get_group_url);
190
-
191
- // use push_group_list to sync each group HEAD to github
192
- for (const push_group of push_group_list) {
193
- const { group } = push_group;
194
-
195
- // move to temporary branch for resetting to lookback_index to create PR
196
- await cli(`git checkout -b ${group.id}`);
197
-
198
- // prepare branch for sync, reset to commit at lookback index
199
- await cli(`git reset --hard HEAD~${push_group.lookback_index}`);
200
-
201
- await sync_group_github({ group, pr_url_list, skip_checkout: true });
202
-
203
- // done, remove temp push branch and move back to temp branch
204
- await cli(`git checkout ${temp_branch_name}`);
205
- await cli(`git branch -D ${group.id}`);
206
- }
207
-
208
- // finally, ensure all prs have the updated stack table from updated pr_url_list
209
- await update_pr_tables(pr_url_list);
210
- }
211
-
212
- async function rebase_cherry_pick() {
213
- actions.debug("rebase_cherry_pick");
214
-
215
- // create temporary branch based on merge base
216
- await cli(`git checkout -b ${temp_branch_name} ${rebase_merge_base}`);
217
-
218
- const pr_url_list = commit_range.group_list.map(get_group_url);
219
-
220
- for (let i = rebase_group_index; i < commit_range.group_list.length; i++) {
221
- const group = commit_range.group_list[i];
222
-
223
- invariant(group.base, "group.base must exist");
224
-
225
- actions.output(
226
- <FormatText
227
- wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
228
- message="Rebasing {group}…"
229
- values={{
230
- group: (
231
- <Brackets>{group.pr?.title || group.title || group.id}</Brackets>
232
- ),
233
- }}
234
- />
235
- );
236
-
237
- // cherry-pick and amend commits one by one
238
- for (const commit of group.commits) {
239
- // ensure clean base to avoid conflicts when applying patch
240
- await cli(`git clean -fd`);
241
-
242
- // create, apply and cleanup patch
243
- await cli(`git format-patch -1 ${commit.sha} --stdout > ${PATCH_FILE}`);
244
- await cli(`git apply ${PATCH_FILE}`);
245
- await cli(`rm ${PATCH_FILE}`);
246
-
247
- // add all changes to stage
248
- await cli(`git add --all`);
249
-
250
- const metadata = { id: group.id, title: group.title };
251
- const new_message = Metadata.write(commit.full_message, metadata);
252
- const git_commit_comand = [`git commit -m "${new_message}"`];
253
-
254
- if (argv.verify === false) {
255
- git_commit_comand.push("--no-verify");
256
- }
257
-
258
- await cli(git_commit_comand);
259
- }
260
-
261
- await sync_group_github({ group, pr_url_list, skip_checkout: false });
262
- }
263
-
264
- // finally, ensure all prs have the updated stack table from updated pr_url_list
265
- await update_pr_tables(pr_url_list);
266
- }
267
-
268
- async function sync_group_github(args: {
269
- group: CommitMetadataGroup;
270
- pr_url_list: Array<string>;
271
- skip_checkout: boolean;
272
- }) {
273
- if (!argv.sync) {
274
- return;
275
- }
276
-
277
- const { group, pr_url_list } = args;
278
-
279
- invariant(group.base, "group.base must exist");
280
-
281
- actions.output(
282
- <FormatText
283
- wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
284
- message="Syncing {group}…"
285
- values={{
286
- group: (
287
- <Brackets>{group.pr?.title || group.title || group.id}</Brackets>
288
- ),
289
- }}
290
- />
291
- );
292
-
293
- // we may temporarily mark PR as a draft before editing it
294
- // if it is not already a draft PR, to avoid notification spam
295
- let is_temp_draft = false;
296
-
297
- // before pushing reset base to master temporarily
298
- // avoid accidentally pointing to orphaned parent commit
299
- // should hopefully fix issues where a PR includes a bunch of commits after pushing
300
- if (group.pr) {
301
- if (!group.pr.isDraft) {
302
- is_temp_draft = true;
303
- }
304
-
305
- if (is_temp_draft) {
306
- await github.pr_draft({
307
- branch: group.id,
308
- draft: true,
309
- });
310
- }
311
-
312
- await github.pr_edit({
313
- branch: group.id,
314
- base: master_branch,
315
- });
316
- }
317
-
318
- // push to origin since github requires commit shas to line up perfectly
319
- const git_push_command = [`git push -f origin HEAD:${group.id}`];
320
-
321
- if (argv.verify === false) {
322
- git_push_command.push("--no-verify");
323
- }
324
-
325
- await cli(git_push_command);
134
+ // after all commits have been modified move the pointer
135
+ // of original branch to the newly created temporary branch
136
+ await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
326
137
 
327
- const selected_url = get_group_url(group);
138
+ restore_git();
328
139
 
329
- if (group.pr) {
330
- // ensure base matches pr in github
331
- await github.pr_edit({
332
- branch: group.id,
333
- base: group.base,
334
- body: StackSummaryTable.write({
335
- body: group.pr.body,
336
- pr_url_list,
337
- selected_url,
338
- }),
140
+ if (argv.sync) {
141
+ actions.set((state) => {
142
+ state.step = "sync-github";
143
+ state.sync_github = { commit_range, rebase_group_index };
339
144
  });
340
-
341
- if (is_temp_draft) {
342
- // mark pr as ready for review again
343
- await github.pr_draft({
344
- branch: group.id,
345
- draft: false,
346
- });
347
- }
348
145
  } else {
349
- if (!args.skip_checkout) {
350
- // delete local group branch if leftover
351
- await cli(`git branch -D ${group.id}`, { ignoreExitCode: true });
352
-
353
- // move to temporary branch for creating pr
354
- await cli(`git checkout -b ${group.id}`);
355
- }
356
-
357
- // create pr in github
358
- const pr_url = await github.pr_create({
359
- branch: group.id,
360
- base: group.base,
361
- title: group.title,
362
- body: DEFAULT_PR_BODY,
363
- draft: argv.draft,
146
+ actions.set((state) => {
147
+ state.step = "post-rebase-status";
364
148
  });
365
-
366
- if (!pr_url) {
367
- throw new Error("unable to create pr");
368
- }
369
-
370
- // update pr_url_list with created pr_url
371
- for (let i = 0; i < pr_url_list.length; i++) {
372
- const url = pr_url_list[i];
373
- if (url === selected_url) {
374
- pr_url_list[i] = pr_url;
375
- }
376
- }
377
-
378
- // move back to temp branch
379
- if (!args.skip_checkout) {
380
- await cli(`git checkout ${temp_branch_name}`);
381
- }
382
149
  }
383
- }
384
-
385
- async function update_pr_tables(pr_url_list: Array<string>) {
386
- if (!argv.sync) {
387
- return;
150
+ } catch (err) {
151
+ if (err instanceof Error) {
152
+ actions.error(err.message);
388
153
  }
389
154
 
390
- for (let i = 0; i < commit_range.group_list.length; i++) {
391
- const group = commit_range.group_list[i];
392
-
393
- // use the updated pr_url_list to get the actual selected_url
394
- const selected_url = pr_url_list[i];
395
-
396
- invariant(group.base, "group.base must exist");
397
-
398
- const body = group.pr?.body || DEFAULT_PR_BODY;
399
-
400
- const update_body = StackSummaryTable.write({
401
- body,
402
- pr_url_list,
403
- selected_url,
404
- });
405
-
406
- if (update_body === body) {
407
- actions.debug(`Skipping body update for ${selected_url}`);
408
- } else {
409
- actions.debug(`Update body for ${selected_url}`);
410
-
411
- await github.pr_edit({
412
- branch: group.id,
413
- base: group.base,
414
- body: update_body,
415
- });
416
- }
155
+ actions.error("Unable to rebase.");
156
+ if (!argv.verbose) {
157
+ actions.error("Try again with `--verbose` to see more information.");
417
158
  }
159
+
160
+ handle_exit(16);
418
161
  }
419
162
 
420
163
  // cleanup git operations if cancelled during manual rebase
@@ -424,9 +167,6 @@ async function run() {
424
167
  // all children processes receive the SIGINT signal
425
168
  const spawn_options = { ignoreExitCode: true };
426
169
 
427
- // always clean up any patch files
428
- cli.sync(`rm ${PATCH_FILE}`, spawn_options);
429
-
430
170
  // always hard reset and clean to allow subsequent checkout
431
171
  // if there are files checkout will fail and cascade fail subsequent commands
432
172
  cli.sync(`git reset --hard`, spawn_options);
@@ -438,13 +178,6 @@ async function run() {
438
178
  // ...and cleanup temporary branch
439
179
  cli.sync(`git branch -D ${temp_branch_name}`, spawn_options);
440
180
 
441
- if (commit_range) {
442
- // ...and cleanup pr group branches
443
- for (const group of commit_range.group_list) {
444
- cli.sync(`git branch -D ${group.id}`, spawn_options);
445
- }
446
- }
447
-
448
181
  // restore back to original dir
449
182
  if (fs.existsSync(cwd)) {
450
183
  process.chdir(cwd);
@@ -452,7 +185,7 @@ async function run() {
452
185
  cli.sync(`pwd`, spawn_options);
453
186
  }
454
187
 
455
- function handle_exit() {
188
+ function handle_exit(code: number) {
456
189
  actions.output(
457
190
  <Ink.Text color={colors.yellow}>
458
191
  Restoring <Brackets>{branch_name}</Brackets>…
@@ -467,11 +200,6 @@ async function run() {
467
200
  </Ink.Text>
468
201
  );
469
202
 
470
- actions.exit(5);
203
+ actions.exit(code);
471
204
  }
472
205
  }
473
-
474
- type CommitMetadataGroup = CommitMetadata.CommitRange["group_list"][number];
475
- const get_group_url = (group: CommitMetadataGroup) => group.pr?.url || group.id;
476
-
477
- const PATCH_FILE = "git-stack-cli-patch.patch";