git-stack-cli 2.8.2 → 2.9.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.
@@ -1,5 +1,7 @@
1
1
  import * as React from "react";
2
2
 
3
+ import path from "node:path";
4
+
3
5
  import * as Ink from "ink-cjs";
4
6
  import last from "lodash/last";
5
7
 
@@ -10,6 +12,7 @@ import { cli } from "~/core/cli";
10
12
  import { colors } from "~/core/colors";
11
13
  import * as github from "~/core/github";
12
14
  import { invariant } from "~/core/invariant";
15
+ import { safe_exists } from "~/core/safe_exists";
13
16
 
14
17
  import type * as CommitMetadata from "~/core/CommitMetadata";
15
18
 
@@ -81,9 +84,7 @@ async function run() {
81
84
  invariant(last_commit, "last_commit must exist");
82
85
 
83
86
  // push group in isolation if master_base is set
84
- // for the first group (i > 0) we can skip this
85
- // since it'll be based off master anyway
86
- if (group.master_base && i > 0) {
87
+ if (group.master_base) {
87
88
  await push_master_group(group);
88
89
  continue;
89
90
  }
@@ -167,10 +168,6 @@ async function run() {
167
168
  }
168
169
 
169
170
  actions.error("Unable to sync.");
170
- if (!argv.verbose) {
171
- actions.error("Try again with `--verbose` to see more information.");
172
- }
173
-
174
171
  actions.exit(15);
175
172
  }
176
173
 
@@ -301,29 +298,43 @@ async function run() {
301
298
  }
302
299
 
303
300
  async function push_master_group(group: CommitMetadataGroup) {
304
- const worktree_path = `.git/git-stack-worktrees/push_master_group`;
305
-
306
- // ensure previous instance of worktree is removed
307
- await cli(`git worktree remove --force ${worktree_path}`, { ignoreExitCode: true });
308
-
309
- // create temp worktree at master (or group.base if you prefer)
310
- await cli(`git worktree add -f ${worktree_path} ${master_branch}`);
301
+ invariant(repo_root, "repo_root must exist");
311
302
 
312
- try {
313
- // cherry-pick the group commits onto that base
314
- const cp_commit_list = group.commits.map((c) => c.sha);
315
- await cli(`git -C ${worktree_path} cherry-pick ${cp_commit_list}`);
316
-
317
- `git -C ${worktree_path} push -f origin HEAD:refs/heads/${group.id}`;
318
-
319
- const push_target = `HEAD:refs/heads/${group.id}`;
320
- const git_push_command = create_git_push_command(`git -C ${worktree_path}`, push_target);
321
-
322
- await cli(git_push_command);
323
- } finally {
324
- // clean up even if push fails
325
- await cli(`git worktree remove --force ${worktree_path}`);
303
+ const worktree_path = `.git/git-stack-worktrees/push_master_group`;
304
+ const worktree_path_absolute = path.join(repo_root, worktree_path);
305
+
306
+ // ensure worktree for pushing master groups
307
+ if (!(await safe_exists(worktree_path_absolute))) {
308
+ actions.output(
309
+ <Ink.Text color={colors.white}>
310
+ Creating <Ink.Text color={colors.yellow}>{worktree_path}</Ink.Text>
311
+ </Ink.Text>,
312
+ );
313
+ actions.output(
314
+ <Ink.Text color={colors.gray}>(this may take a moment the first time…)</Ink.Text>,
315
+ );
316
+ await cli(`git worktree add -f ${worktree_path} ${master_branch}`);
326
317
  }
318
+
319
+ // ensure worktree is clean + on the right base before applying commits
320
+ // - abort any in-progress cherry-pick/rebase
321
+ // - drop local changes/untracked files (including ignored) for a truly fresh state
322
+ // - reset to the desired base
323
+ await cli(`git -C ${worktree_path} cherry-pick --abort`, { ignoreExitCode: true });
324
+ await cli(`git -C ${worktree_path} rebase --abort`, { ignoreExitCode: true });
325
+ await cli(`git -C ${worktree_path} merge --abort`, { ignoreExitCode: true });
326
+ await cli(`git -C ${worktree_path} checkout -f ${master_branch}`);
327
+ await cli(`git -C ${worktree_path} reset --hard ${master_branch}`);
328
+ await cli(`git -C ${worktree_path} clean -fd`);
329
+
330
+ // cherry-pick the group commits onto that base
331
+ const cp_commit_list = group.commits.map((c) => c.sha).join(" ");
332
+ await cli(`git -C ${worktree_path} cherry-pick ${cp_commit_list}`);
333
+
334
+ const push_target = `HEAD:refs/heads/${group.id}`;
335
+ const git_push_command = create_git_push_command(`git -C ${worktree_path}`, push_target);
336
+
337
+ await cli(git_push_command);
327
338
  }
328
339
  }
329
340
 
package/src/command.ts CHANGED
@@ -160,20 +160,6 @@ const DefaultOptions = {
160
160
  "Disable with --no-template",
161
161
  ].join("\n"),
162
162
  },
163
-
164
- "write-state-json": {
165
- hidden: true,
166
- type: "boolean",
167
- default: false,
168
- description: "Write state to local json file for debugging",
169
- },
170
-
171
- "mock-metadata": {
172
- hidden: true,
173
- type: "boolean",
174
- default: false,
175
- description: "Mock local store metadata for testing",
176
- },
177
163
  } satisfies YargsOptions;
178
164
 
179
165
  const FixupOptions = {
@@ -184,7 +184,7 @@ Rebase.run = async function run(props: Props) {
184
184
  // always hard reset and clean to allow subsequent checkout
185
185
  // if there are files checkout will fail and cascade fail subsequent commands
186
186
  cli.sync(`git reset --hard`, spawn_options);
187
- cli.sync(`git clean -df`, spawn_options);
187
+ cli.sync(`git clean -fd`, spawn_options);
188
188
 
189
189
  // always put self back in original branch
190
190
  cli.sync(`git checkout ${branch_name}`, spawn_options);
@@ -163,21 +163,57 @@ export async function range(commit_group_map?: CommitGroupMap) {
163
163
  // console.debug(" ", "group.base", group.base);
164
164
  }
165
165
 
166
+ // console.debug({ group });
167
+
166
168
  if (!group.pr) {
167
169
  group.dirty = true;
168
170
  } else {
169
171
  if (group.pr.baseRefName !== group.base) {
172
+ // console.debug("PR_BASEREF_MISMATCH");
170
173
  group.dirty = true;
171
- } else if (group.master_base && i > 0) {
174
+ } else if (group.master_base) {
175
+ // console.debug("MASTER_BASE_DIFF_COMPARE");
176
+
172
177
  // special case
173
178
  // master_base groups cannot be compared by commit sha
174
179
  // instead compare the literal diff local against origin
175
180
  // gh pr diff --color=never 110
176
181
  // git --no-pager diff --color=never 00c8fe0~1..00c8fe0
177
- const diff_github = await github.pr_diff(group.pr.number);
178
- const diff_local = await git.get_diff(group.commits);
179
- if (diff_github !== diff_local) {
182
+ let diff_github = await github.pr_diff(group.pr.number);
183
+ diff_github = diff_strip_index_lines(diff_github);
184
+
185
+ let diff_local = await git.get_diff(group.commits);
186
+ diff_local = diff_strip_index_lines(diff_local);
187
+
188
+ // find the first differing character index
189
+ let compare_length = Math.min(diff_github.length, diff_local.length);
190
+ let diff_index = -1;
191
+ for (let c_i = 0; c_i < compare_length; c_i++) {
192
+ if (diff_github[c_i] !== diff_local[c_i]) {
193
+ diff_index = c_i;
194
+ break;
195
+ }
196
+ }
197
+ if (diff_index > -1) {
180
198
  group.dirty = true;
199
+
200
+ // // print preview at diff_index for both strings
201
+ // const preview_radius = 30;
202
+ // const start_index = Math.max(0, diff_index - preview_radius);
203
+ // const end_index = Math.min(compare_length, diff_index + preview_radius);
204
+
205
+ // diff_github = diff_github.substring(start_index, end_index);
206
+ // diff_github = JSON.stringify(diff_github).slice(1, -1);
207
+
208
+ // diff_local = diff_local.substring(start_index, end_index);
209
+ // diff_local = JSON.stringify(diff_local).slice(1, -1);
210
+
211
+ // let pointer_indent = " ".repeat(diff_index - start_index + 1);
212
+ // console.warn(`⚠️ git diff mismatch`);
213
+ // console.warn(` ${pointer_indent}⌄`);
214
+ // console.warn(`diff_github …${diff_github}…`);
215
+ // console.warn(`diff_local …${diff_local}…`);
216
+ // console.warn(` ${pointer_indent}⌃`);
181
217
  }
182
218
  } else if (!group.master_base && previous_group && previous_group.master_base) {
183
219
  // special case
@@ -197,8 +233,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
197
233
 
198
234
  // compare all commits against pr commits
199
235
  if (group.pr.commits.length !== all_commits.length) {
236
+ // console.debug("BOUNDARY_COMMIT_LENGTH_MISMATCH");
200
237
  group.dirty = true;
201
238
  } else {
239
+ // console.debug("BOUNDARY_COMMIT_SHA_COMPARISON");
202
240
  for (let i = 0; i < group.pr.commits.length; i++) {
203
241
  const pr_commit = group.pr.commits[i];
204
242
  const local_commit = all_commits[i];
@@ -209,8 +247,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
209
247
  }
210
248
  }
211
249
  } else if (group.pr.commits.length !== group.commits.length) {
250
+ // console.debug("COMMIT_LENGTH_MISMATCH");
212
251
  group.dirty = true;
213
252
  } else {
253
+ // console.debug("COMMIT_SHA_COMPARISON");
214
254
  // if we still haven't marked this dirty, check each commit
215
255
  // comapre literal commit shas in group
216
256
  for (let i = 0; i < group.pr.commits.length; i++) {
@@ -239,3 +279,11 @@ export async function range(commit_group_map?: CommitGroupMap) {
239
279
  }
240
280
 
241
281
  export const UNASSIGNED = "unassigned";
282
+
283
+ function diff_strip_index_lines(diff_text: string) {
284
+ return diff_text.replace(RE.diff_index_line, "");
285
+ }
286
+
287
+ const RE = {
288
+ diff_index_line: /^index [0-9a-f]+\.\.[0-9a-f]+.*?\n/gm,
289
+ };