@forwardimpact/libutil 0.1.95 → 0.1.96

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/git-client.js +66 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libutil",
3
- "version": "0.1.95",
3
+ "version": "0.1.96",
4
4
  "description": "Cross-cutting utilities: retry, hashing, token counting, and project discovery.",
5
5
  "keywords": [
6
6
  "util",
package/src/git-client.js CHANGED
@@ -337,10 +337,48 @@ export class GitClient {
337
337
  return result.stdout.trim();
338
338
  }
339
339
 
340
+ /**
341
+ * Push `branch` to `remote` with the machine-readable per-ref status
342
+ * (`--porcelain`). Runs with `allowFailure` so the caller classifies the
343
+ * outcome from the remote-originated per-ref line rather than the exit code:
344
+ * the line is `<flag>\t<src>:<dst>\t<summary>`, flag ` `/`=` accepted,
345
+ * `!` rejected.
346
+ */
347
+ async pushPorcelain(remote = "origin", branch, { cwd } = {}) {
348
+ const args = ["push", "--porcelain", remote];
349
+ if (branch) args.push(branch);
350
+ return this.#runRaw(args, { cwd, allowFailure: true });
351
+ }
352
+
353
+ /**
354
+ * The commit SHA the remote ref points at, read fresh with `ls-remote`.
355
+ * Throws a {@link GitError} on transport failure; returns "" when the ref
356
+ * does not exist on the remote.
357
+ */
358
+ async remoteRefTip(remote = "origin", branch, { cwd } = {}) {
359
+ const r = await this.#runRaw(["ls-remote", remote, branch], { cwd });
360
+ return r.stdout.split("\t")[0]?.trim() ?? "";
361
+ }
362
+
363
+ /** Whether `ancestor` is an ancestor of `descendant` (`merge-base --is-ancestor`). */
364
+ async isAncestor(ancestor, descendant, { cwd } = {}) {
365
+ const r = await this.#runRaw(
366
+ ["merge-base", "--is-ancestor", ancestor, descendant],
367
+ { cwd, allowFailure: true },
368
+ );
369
+ return r.exitCode === 0;
370
+ }
371
+
372
+ /** `git status --porcelain` output (for unmerged-path detection). */
373
+ async statusPorcelain({ cwd } = {}) {
374
+ return this.#runRaw(["status", "--porcelain"], { cwd });
375
+ }
376
+
340
377
  /**
341
378
  * The short name of the branch HEAD points at, or "" when HEAD is detached.
342
379
  * An unborn HEAD on a branch (no commits yet) still returns that branch name —
343
380
  * `symbolic-ref` reads the ref HEAD targets, not whether it resolves.
381
+ * `symbolic-ref -q HEAD` exits non-zero on a detached HEAD, swallowed here.
344
382
  */
345
383
  async headBranch({ cwd } = {}) {
346
384
  const r = await this.#runRaw(["symbolic-ref", "--short", "-q", "HEAD"], {
@@ -413,6 +451,34 @@ export class GitClient {
413
451
  });
414
452
  }
415
453
 
454
+ /**
455
+ * Resolve `ref` to a commit SHA, or "" when it does not resolve
456
+ * (`rev-parse --verify -q`, swallowed).
457
+ */
458
+ async revParse(ref, { cwd } = {}) {
459
+ const r = await this.#runRaw(["rev-parse", "--verify", "-q", ref], {
460
+ cwd,
461
+ allowFailure: true,
462
+ });
463
+ return r.exitCode === 0 ? r.stdout.trim() : "";
464
+ }
465
+
466
+ /**
467
+ * Name-status lines between two tree-ish (`<code>\t<path>`). Codes read
468
+ * going `a`→`b`: a path present in `a` but gone in `b` is `D`, modified `M`,
469
+ * added `A`. The conservation guard calls it tip-first (a = remote tip,
470
+ * b = HEAD) so a dropped foreign file reads `D`.
471
+ */
472
+ async diffNameStatus(a, b, { cwd } = {}) {
473
+ const r = await this.#runRaw(["diff", "--name-status", a, b], { cwd });
474
+ return r.stdout.trim();
475
+ }
476
+
477
+ /** Drop a stash addressed by SHA, never by stack position (`stash drop <sha>`). */
478
+ async stashDropBySha(sha, { cwd } = {}) {
479
+ return this.#runRaw(["stash", "drop", sha], { cwd, allowFailure: true });
480
+ }
481
+
416
482
  /** Return a new client that threads `token` into the git env. */
417
483
  withAuth(token) {
418
484
  return new GitClient({ runtime: this.#runtime, token });