@devframes/service-git 0.9.4 → 0.9.5

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/dist/index.d.mts CHANGED
@@ -56,6 +56,8 @@ interface LogArgs {
56
56
  skip?: number;
57
57
  /** Optional ref/branch to read history from (default: current HEAD). */
58
58
  ref?: string;
59
+ /** Restrict history to commits that touched these repo-relative path(s). */
60
+ paths?: string[];
59
61
  }
60
62
  interface Branch {
61
63
  name: string;
@@ -73,6 +75,47 @@ interface GitBranches {
73
75
  current: string | null;
74
76
  branches: Branch[];
75
77
  }
78
+ interface Tag {
79
+ name: string;
80
+ /** Short SHA of the commit the tag ultimately points to. */
81
+ sha: string;
82
+ /**
83
+ * Tag creation date as epoch milliseconds — the tag's own date for
84
+ * annotated tags, the target commit's date for lightweight tags. `0` when
85
+ * the date can't be parsed.
86
+ */
87
+ date: number;
88
+ /** Tag message subject (annotated) or target commit subject (lightweight). */
89
+ subject: string;
90
+ /** `true` for annotated tags, which carry their own message and date. */
91
+ annotated: boolean;
92
+ }
93
+ interface GitTags {
94
+ isRepo: boolean;
95
+ /** Tags, newest creation date first. */
96
+ tags: Tag[];
97
+ }
98
+ interface ReadFileArgs {
99
+ /** Repo-relative path to the file. */
100
+ path: string;
101
+ /** Commit-ish to read the file from (default: current HEAD). */
102
+ ref?: string;
103
+ }
104
+ interface GitFile {
105
+ /** `false` when the working directory is not inside a git repository. */
106
+ isRepo: boolean;
107
+ /** `false` when no blob exists at `path` for `ref`. */
108
+ found: boolean;
109
+ /** The resolved ref the file was read from. */
110
+ ref: string;
111
+ path: string;
112
+ /** File text, or `null` when not found or binary. */
113
+ content: string | null;
114
+ /** `true` when the blob is binary (its `content` is omitted). */
115
+ binary: boolean;
116
+ /** `true` when `content` was clipped to the internal char limit. */
117
+ truncated: boolean;
118
+ }
76
119
  interface DiffFile {
77
120
  path: string;
78
121
  additions: number;
@@ -165,8 +208,10 @@ interface GitServiceApi {
165
208
  status: () => Promise<GitStatus>;
166
209
  log: (args?: LogArgs) => Promise<GitLog>;
167
210
  show: (args: ShowArgs) => Promise<CommitDetail>;
211
+ readFile: (args: ReadFileArgs) => Promise<GitFile>;
168
212
  diff: (args?: DiffArgs) => Promise<GitDiff>;
169
213
  branches: () => Promise<GitBranches>;
214
+ tags: () => Promise<GitTags>;
170
215
  stage: (args: StageArgs) => Promise<GitStatus>;
171
216
  unstage: (args: UnstageArgs) => Promise<GitStatus>;
172
217
  commit: (args: CommitArgs) => Promise<CommitResult>;
@@ -187,8 +232,10 @@ declare module 'devframe' {
187
232
  'devframes:service:git:status': () => Promise<GitStatus>;
188
233
  'devframes:service:git:log': (args?: LogArgs) => Promise<GitLog>;
189
234
  'devframes:service:git:show': (args: ShowArgs) => Promise<CommitDetail>;
235
+ 'devframes:service:git:readFile': (args: ReadFileArgs) => Promise<GitFile>;
190
236
  'devframes:service:git:diff': (args?: DiffArgs) => Promise<GitDiff>;
191
237
  'devframes:service:git:branches': () => Promise<GitBranches>;
238
+ 'devframes:service:git:tags': () => Promise<GitTags>;
192
239
  'devframes:service:git:stage': (args: StageArgs) => Promise<GitStatus>;
193
240
  'devframes:service:git:unstage': (args: UnstageArgs) => Promise<GitStatus>;
194
241
  'devframes:service:git:commit': (args: CommitArgs) => Promise<CommitResult>;
@@ -211,4 +258,4 @@ declare module 'devframe' {
211
258
  */
212
259
  declare function createGitService(options?: GitServiceOptions): DevframeServiceDefinition<GitServiceApi, GitServiceOptions>;
213
260
  //#endregion
214
- export { Branch, Commit, CommitArgs, CommitDetail, CommitFile, CommitResult, DiffArgs, DiffFile, FileStatusCode, GIT_SERVICE_PACKAGE, GIT_SERVICE_SCOPE, GitBranches, GitDiff, GitLog, GitServiceApi, GitServiceOptions, GitStatus, LogArgs, ShowArgs, StageArgs, StatusFileEntry, UnstageArgs, createGitService, createGitService as default };
261
+ export { Branch, Commit, CommitArgs, CommitDetail, CommitFile, CommitResult, DiffArgs, DiffFile, FileStatusCode, GIT_SERVICE_PACKAGE, GIT_SERVICE_SCOPE, GitBranches, GitDiff, GitFile, GitLog, GitServiceApi, GitServiceOptions, GitStatus, GitTags, LogArgs, ReadFileArgs, ShowArgs, StageArgs, StatusFileEntry, Tag, UnstageArgs, createGitService, createGitService as default };
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { execFile } from "node:child_process";
4
4
  import process from "node:process";
5
5
  import { promisify } from "node:util";
6
6
  //#region package.json
7
- var version = "0.9.4";
7
+ var version = "0.9.5";
8
8
  //#endregion
9
9
  //#region src/git.ts
10
10
  const execFileAsync = promisify(execFile);
@@ -66,6 +66,8 @@ function gitErrorMessage(error) {
66
66
  //#region src/operations.ts
67
67
  /** Hard cap on returned patch text to keep payloads bounded. */
68
68
  const PATCH_CHAR_LIMIT = 2e5;
69
+ /** Hard cap on returned raw file content to keep payloads bounded. */
70
+ const FILE_CHAR_LIMIT = 5e5;
69
71
  const EMPTY_STATUS = {
70
72
  isRepo: false,
71
73
  root: null,
@@ -203,6 +205,14 @@ const BRANCH_FORMAT = [
203
205
  "%(upstream:track)",
204
206
  "%(contents:subject)"
205
207
  ].join("");
208
+ const TAG_FORMAT = [
209
+ "%(refname:short)",
210
+ "%(objecttype)",
211
+ "%(objectname:short)",
212
+ "%(*objectname:short)",
213
+ "%(creatordate:iso-strict)",
214
+ "%(contents:subject)"
215
+ ].join("");
206
216
  function parseTrack(track) {
207
217
  if (track.includes("gone")) return {
208
218
  ahead: 0,
@@ -299,15 +309,22 @@ function parseCommitNumstat(raw, status) {
299
309
  };
300
310
  });
301
311
  }
302
- function clipPatch(raw) {
303
- return raw.length > PATCH_CHAR_LIMIT ? {
304
- patch: raw.slice(0, PATCH_CHAR_LIMIT),
312
+ function clipText(raw, limit) {
313
+ return raw.length > limit ? {
314
+ text: raw.slice(0, limit),
305
315
  truncated: true
306
316
  } : {
307
- patch: raw,
317
+ text: raw,
308
318
  truncated: false
309
319
  };
310
320
  }
321
+ function clipPatch(raw) {
322
+ const { text, truncated } = clipText(raw, PATCH_CHAR_LIMIT);
323
+ return {
324
+ patch: text,
325
+ truncated
326
+ };
327
+ }
311
328
  /**
312
329
  * Build the git node API bound to a single working directory, with the repo
313
330
  * root resolved once (memoized). This is the surface returned to in-process
@@ -431,6 +448,8 @@ function createGitOps(cwd) {
431
448
  };
432
449
  command.push("--end-of-options", ref);
433
450
  }
451
+ const paths = (args.paths ?? []).map((p) => p.trim()).filter(Boolean);
452
+ if (paths.length > 0) command.push("--", ...paths);
434
453
  const raw = await tryGit(cwd, command);
435
454
  const commits = raw ? parseLog(raw) : [];
436
455
  return {
@@ -447,6 +466,44 @@ function createGitOps(cwd) {
447
466
  if (!await resolveRoot() || !hash) return { ...EMPTY_DETAIL };
448
467
  return readCommit(hash, includePatch);
449
468
  },
469
+ async readFile(args) {
470
+ const path = (args?.path ?? "").trim();
471
+ const ref = args?.ref?.trim() || "HEAD";
472
+ const root = await resolveRoot();
473
+ const base = {
474
+ isRepo: !!root,
475
+ found: false,
476
+ ref,
477
+ path,
478
+ content: null,
479
+ binary: false,
480
+ truncated: false
481
+ };
482
+ if (!root || !path) return base;
483
+ if (!isSafeRevision(ref)) return base;
484
+ let raw;
485
+ try {
486
+ ({stdout: raw} = await runGit(cwd, [
487
+ "show",
488
+ "--end-of-options",
489
+ `${ref}:${path}`
490
+ ]));
491
+ } catch {
492
+ return base;
493
+ }
494
+ if (raw.includes("\0")) return {
495
+ ...base,
496
+ found: true,
497
+ binary: true
498
+ };
499
+ const { text: content, truncated } = clipText(raw, FILE_CHAR_LIMIT);
500
+ return {
501
+ ...base,
502
+ found: true,
503
+ content,
504
+ truncated
505
+ };
506
+ },
450
507
  async diff(args = {}) {
451
508
  const { path, staged = false } = args;
452
509
  if (!await resolveRoot()) return {
@@ -521,6 +578,38 @@ function createGitOps(cwd) {
521
578
  branches
522
579
  };
523
580
  },
581
+ async tags() {
582
+ if (!await resolveRoot()) return {
583
+ isRepo: false,
584
+ tags: []
585
+ };
586
+ const raw = await tryGit(cwd, [
587
+ "for-each-ref",
588
+ `--format=${TAG_FORMAT}`,
589
+ "refs/tags"
590
+ ]);
591
+ if (!raw) return {
592
+ isRepo: true,
593
+ tags: []
594
+ };
595
+ const tags = splitClean(raw, "\n").map((line) => {
596
+ const [name, objectType, objectSha, targetSha, isoDate, subject] = line.split("");
597
+ const annotated = objectType === "tag";
598
+ const parsed = Date.parse(isoDate);
599
+ return {
600
+ name,
601
+ sha: targetSha || objectSha,
602
+ date: Number.isNaN(parsed) ? 0 : parsed,
603
+ subject: subject ?? "",
604
+ annotated
605
+ };
606
+ });
607
+ tags.sort((a, b) => b.date - a.date);
608
+ return {
609
+ isRepo: true,
610
+ tags
611
+ };
612
+ },
524
613
  async stage(args) {
525
614
  const paths = args?.paths ?? [];
526
615
  if (await resolveRoot() && paths.length > 0) await runGit(cwd, [
@@ -640,6 +729,15 @@ const commitDetailSchema = s.object({
640
729
  patch: s.nullable(s.string()),
641
730
  truncated: s.boolean()
642
731
  });
732
+ const gitFileSchema = s.object({
733
+ isRepo: s.boolean(),
734
+ found: s.boolean(),
735
+ ref: s.string(),
736
+ path: s.string(),
737
+ content: s.nullable(s.string()),
738
+ binary: s.boolean(),
739
+ truncated: s.boolean()
740
+ });
643
741
  const gitDiffSchema = s.object({
644
742
  isRepo: s.boolean(),
645
743
  staged: s.boolean(),
@@ -689,12 +787,13 @@ function createGitService(options) {
689
787
  args: [s.object({
690
788
  limit: s.optional(s.number()),
691
789
  skip: s.optional(s.number()),
692
- ref: s.optional(s.string())
790
+ ref: s.optional(s.string()),
791
+ paths: s.optional(s.array(s.string()))
693
792
  })],
694
793
  returns: gitLogSchema,
695
794
  agent: {
696
795
  title: "Git log",
697
- description: "Commit history of the inspected repository, newest first. Paginate with limit (1-200, default 30) and skip; pass ref to read another branch. Safe to call freely."
796
+ description: "Commit history of the inspected repository, newest first. Paginate with limit (1-200, default 30) and skip; pass ref to read another branch, or paths to list only commits that touched those files/directories. Safe to call freely."
698
797
  },
699
798
  handler: (args = {}) => ops.log(args)
700
799
  }));
@@ -713,6 +812,21 @@ function createGitService(options) {
713
812
  },
714
813
  handler: (args) => ops.show(args)
715
814
  }));
815
+ ctx.rpc.register(defineRpcFunction({
816
+ name: "readFile",
817
+ type: "query",
818
+ jsonSerializable: true,
819
+ args: [s.object({
820
+ path: s.string(),
821
+ ref: s.optional(s.string())
822
+ })],
823
+ returns: gitFileSchema,
824
+ agent: {
825
+ title: "Git read file",
826
+ description: "Read the contents of a single file at a commit-ish (default HEAD) — the raw text of a versioned file without checking it out. found is false when no such file exists at the ref; binary blobs return with content omitted. Safe to call freely."
827
+ },
828
+ handler: (args) => ops.readFile(args)
829
+ }));
716
830
  ctx.rpc.register(defineRpcFunction({
717
831
  name: "diff",
718
832
  type: "query",
@@ -738,6 +852,16 @@ function createGitService(options) {
738
852
  },
739
853
  handler: () => ops.branches()
740
854
  }));
855
+ ctx.rpc.register(defineRpcFunction({
856
+ name: "tags",
857
+ type: "query",
858
+ jsonSerializable: true,
859
+ agent: {
860
+ title: "Git tags",
861
+ description: "List tags (newest first) with the target commit SHA, creation date, and message subject; annotated tags are flagged. Safe to call freely."
862
+ },
863
+ handler: () => ops.tags()
864
+ }));
741
865
  ctx.rpc.register(defineRpcFunction({
742
866
  name: "stage",
743
867
  type: "action",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devframes/service-git",
3
3
  "type": "module",
4
- "version": "0.9.4",
4
+ "version": "0.9.5",
5
5
  "description": "Devframe wire service exposing read/write git operations (status, log, show, diff, branches, stage, unstage, commit) over RPC.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -28,13 +28,13 @@
28
28
  "dist"
29
29
  ],
30
30
  "peerDependencies": {
31
- "devframe": "0.9.4"
31
+ "devframe": "0.9.5"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^26.2.0",
35
35
  "tsdown": "^0.22.14",
36
36
  "vitest": "^4.1.10",
37
- "devframe": "0.9.4"
37
+ "devframe": "0.9.5"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsdown",