@devframes/plugin-git 0.9.1 → 0.9.3
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/README.md +23 -21
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +6 -184
- package/dist/index.mjs +1 -1
- package/dist/src-E7rY0DjA.mjs +74 -0
- package/package.json +13 -10
- package/dist/src-CyRmAQrb.mjs +0 -933
package/README.md
CHANGED
|
@@ -9,9 +9,10 @@ repository dashboard with a **Next.js App Router + shadcn/ui** SPA over
|
|
|
9
9
|
type-safe RPC. The host process shells out to `git` and exposes the repository;
|
|
10
10
|
the same bundle runs as a live dev server or a fully static deployment.
|
|
11
11
|
|
|
12
|
-
Status, a SourceTree-style **commit graph**, branches, and diffs
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
Status, a SourceTree-style **commit graph**, branches, and diffs, plus staging,
|
|
13
|
+
unstaging, and committing — all through the shared
|
|
14
|
+
[`@devframes/service-git`](../../services/git) wire service. The UI follows the
|
|
15
|
+
system **light/dark** preference with a manual toggle.
|
|
15
16
|
|
|
16
17
|
## Install
|
|
17
18
|
|
|
@@ -25,7 +26,6 @@ Run the dashboard against the current repository:
|
|
|
25
26
|
|
|
26
27
|
```sh
|
|
27
28
|
pnpx @devframes/plugin-git # dev server (live RPC over WebSocket)
|
|
28
|
-
pnpx @devframes/plugin-git --write # also enable staging / committing from the UI
|
|
29
29
|
pnpx @devframes/plugin-git build # static deploy → dist-static/
|
|
30
30
|
pnpx @devframes/plugin-git --port 4000
|
|
31
31
|
```
|
|
@@ -48,30 +48,32 @@ await createCac(createGitDevframe({ repoRoot: process.cwd() })).parse()
|
|
|
48
48
|
| `basePath` | adapter-resolved | Mount path (`/` standalone, `/__git/` hosted). |
|
|
49
49
|
| `distDir` | bundled SPA | Override the served SPA directory. |
|
|
50
50
|
| `port` | `9710` | Preferred dev-server port. |
|
|
51
|
-
| `write` | `false` | Enable staging, unstaging, and committing from the UI. |
|
|
52
51
|
|
|
53
52
|
## RPC surface
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
All git work runs through the [`@devframes/service-git`](../../services/git)
|
|
55
|
+
wire service, which this devframe declares (`services`) and its SPA calls
|
|
56
|
+
directly over `devframes:service:git:*`. The read functions are `query`
|
|
57
|
+
functions that degrade to an empty, `isRepo: false` result outside a git
|
|
58
|
+
repository; the definition opts them into the static build via `rpc.snapshot`
|
|
59
|
+
(resolved live over WebSocket in dev, served from a build-time snapshot for
|
|
60
|
+
static deploys).
|
|
61
|
+
|
|
62
|
+
- `devframes:service:git:status` — branch, upstream tracking (ahead/behind), staged / unstaged /
|
|
63
|
+
untracked files, parsed from `git status --porcelain=v2`.
|
|
64
|
+
- `devframes:service:git:log` — paginated commit history (`limit` / `skip`) including parent
|
|
63
65
|
hashes, which drive the commit graph.
|
|
64
|
-
- `devframes:
|
|
65
|
-
- `devframes:
|
|
66
|
+
- `devframes:service:git:branches` — local branches with SHA, upstream, ahead/behind, tip subject.
|
|
67
|
+
- `devframes:service:git:diff` — per-file added/deleted counts for the working tree or index, plus
|
|
66
68
|
a unified patch for a selected file.
|
|
67
69
|
|
|
68
|
-
Write actions are `action` functions
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
Write actions are `action` functions — always exposed by the service, with
|
|
71
|
+
write authorization governed by the host's connection-trust boundary. Each
|
|
72
|
+
returns fresh status (commit returns a result):
|
|
71
73
|
|
|
72
|
-
- `devframes:
|
|
73
|
-
- `devframes:
|
|
74
|
-
- `devframes:
|
|
74
|
+
- `devframes:service:git:stage` — `git add` the given paths.
|
|
75
|
+
- `devframes:service:git:unstage` — `git restore --staged` the given paths.
|
|
76
|
+
- `devframes:service:git:commit` — commit the staged changes with a message.
|
|
75
77
|
|
|
76
78
|
## Develop
|
|
77
79
|
|
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,180 +1,4 @@
|
|
|
1
1
|
import { DevframeDefinition } from "devframe";
|
|
2
|
-
//#region src/rpc/functions/branches.d.ts
|
|
3
|
-
interface Branch {
|
|
4
|
-
name: string;
|
|
5
|
-
current: boolean;
|
|
6
|
-
sha: string;
|
|
7
|
-
upstream: string | null;
|
|
8
|
-
subject: string;
|
|
9
|
-
ahead: number;
|
|
10
|
-
behind: number;
|
|
11
|
-
/** `true` when the upstream branch no longer exists. */
|
|
12
|
-
gone: boolean;
|
|
13
|
-
}
|
|
14
|
-
interface GitBranches {
|
|
15
|
-
isRepo: boolean;
|
|
16
|
-
current: string | null;
|
|
17
|
-
branches: Branch[];
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
|
-
//#region src/rpc/functions/status.d.ts
|
|
21
|
-
type FileStatusCode = 'modified' | 'added' | 'deleted' | 'renamed' | 'copied' | 'type-changed' | 'unmerged' | 'unknown';
|
|
22
|
-
interface StatusFileEntry {
|
|
23
|
-
path: string;
|
|
24
|
-
/** Previous path, present for renames and copies. */
|
|
25
|
-
from?: string;
|
|
26
|
-
status: FileStatusCode;
|
|
27
|
-
}
|
|
28
|
-
interface GitStatus {
|
|
29
|
-
/** `false` when the working directory is not inside a git repository. */
|
|
30
|
-
isRepo: boolean;
|
|
31
|
-
root: string | null;
|
|
32
|
-
/** Current branch name, or `null` when HEAD is detached. */
|
|
33
|
-
branch: string | null;
|
|
34
|
-
detached: boolean;
|
|
35
|
-
/** Short HEAD object name. */
|
|
36
|
-
head: string | null;
|
|
37
|
-
upstream: string | null;
|
|
38
|
-
ahead: number;
|
|
39
|
-
behind: number;
|
|
40
|
-
staged: StatusFileEntry[];
|
|
41
|
-
unstaged: StatusFileEntry[];
|
|
42
|
-
untracked: string[];
|
|
43
|
-
/** `true` when there are no staged, unstaged, or untracked changes. */
|
|
44
|
-
clean: boolean;
|
|
45
|
-
/** `true` when stage / unstage / commit actions are available. */
|
|
46
|
-
canWrite: boolean;
|
|
47
|
-
}
|
|
48
|
-
//#endregion
|
|
49
|
-
//#region src/rpc/functions/commit.d.ts
|
|
50
|
-
interface CommitArgs {
|
|
51
|
-
/** Commit message. */
|
|
52
|
-
message: string;
|
|
53
|
-
}
|
|
54
|
-
interface CommitResult {
|
|
55
|
-
/** `true` when the commit succeeded. */
|
|
56
|
-
ok: boolean;
|
|
57
|
-
/** Short hash of the new commit, or `null` on failure. */
|
|
58
|
-
hash: string | null;
|
|
59
|
-
/** Human-readable outcome (e.g. "nothing to commit"). */
|
|
60
|
-
message: string;
|
|
61
|
-
/** Working-tree status after the attempt. */
|
|
62
|
-
status: GitStatus;
|
|
63
|
-
}
|
|
64
|
-
//#endregion
|
|
65
|
-
//#region src/rpc/functions/diff.d.ts
|
|
66
|
-
interface DiffFile {
|
|
67
|
-
path: string;
|
|
68
|
-
additions: number;
|
|
69
|
-
deletions: number;
|
|
70
|
-
binary: boolean;
|
|
71
|
-
}
|
|
72
|
-
interface GitDiff {
|
|
73
|
-
isRepo: boolean;
|
|
74
|
-
staged: boolean;
|
|
75
|
-
path: string | null;
|
|
76
|
-
files: DiffFile[];
|
|
77
|
-
totalAdditions: number;
|
|
78
|
-
totalDeletions: number;
|
|
79
|
-
/** Unified patch text — populated when `path` targets a single file. */
|
|
80
|
-
patch: string | null;
|
|
81
|
-
/** `true` when `patch` was clipped to {@link PATCH_CHAR_LIMIT}. */
|
|
82
|
-
truncated: boolean;
|
|
83
|
-
}
|
|
84
|
-
interface DiffArgs {
|
|
85
|
-
/** Limit the diff to a single path; omit for the whole tree. */
|
|
86
|
-
path?: string;
|
|
87
|
-
/** Diff the index against HEAD instead of the working tree. */
|
|
88
|
-
staged?: boolean;
|
|
89
|
-
}
|
|
90
|
-
//#endregion
|
|
91
|
-
//#region src/rpc/functions/log.d.ts
|
|
92
|
-
interface Commit {
|
|
93
|
-
hash: string;
|
|
94
|
-
shortHash: string;
|
|
95
|
-
author: string;
|
|
96
|
-
email: string;
|
|
97
|
-
/** Author date as epoch milliseconds. */
|
|
98
|
-
date: number;
|
|
99
|
-
subject: string;
|
|
100
|
-
body: string;
|
|
101
|
-
/** Ref names pointing at this commit (branches, tags, HEAD). */
|
|
102
|
-
refs: string[];
|
|
103
|
-
/** Full parent hashes — drives the commit graph. */
|
|
104
|
-
parents: string[];
|
|
105
|
-
}
|
|
106
|
-
interface GitLog {
|
|
107
|
-
isRepo: boolean;
|
|
108
|
-
commits: Commit[];
|
|
109
|
-
limit: number;
|
|
110
|
-
skip: number;
|
|
111
|
-
/** `true` when the page filled to `limit`, hinting at further history. */
|
|
112
|
-
hasMore: boolean;
|
|
113
|
-
}
|
|
114
|
-
interface LogArgs {
|
|
115
|
-
/** Number of commits to return (clamped to 1–200, default 30). */
|
|
116
|
-
limit?: number;
|
|
117
|
-
/** Commits to skip from the tip, for pagination (default 0). */
|
|
118
|
-
skip?: number;
|
|
119
|
-
/** Optional ref/branch to read history from (default: current HEAD). */
|
|
120
|
-
ref?: string;
|
|
121
|
-
}
|
|
122
|
-
//#endregion
|
|
123
|
-
//#region src/rpc/functions/show.d.ts
|
|
124
|
-
interface CommitFile {
|
|
125
|
-
path: string;
|
|
126
|
-
additions: number;
|
|
127
|
-
deletions: number;
|
|
128
|
-
binary: boolean;
|
|
129
|
-
/** Change kind relative to the parent (add / modify / delete / rename …). */
|
|
130
|
-
status: FileStatusCode;
|
|
131
|
-
}
|
|
132
|
-
interface CommitDetail {
|
|
133
|
-
/** `false` when the working directory is not inside a git repository. */
|
|
134
|
-
isRepo: boolean;
|
|
135
|
-
/** `false` when the hash does not resolve to a commit. */
|
|
136
|
-
found: boolean;
|
|
137
|
-
hash: string;
|
|
138
|
-
shortHash: string;
|
|
139
|
-
author: string;
|
|
140
|
-
email: string;
|
|
141
|
-
/** Author date as epoch milliseconds. */
|
|
142
|
-
date: number;
|
|
143
|
-
committer: string;
|
|
144
|
-
committerEmail: string;
|
|
145
|
-
/** Commit date as epoch milliseconds. */
|
|
146
|
-
commitDate: number;
|
|
147
|
-
subject: string;
|
|
148
|
-
body: string;
|
|
149
|
-
parents: string[];
|
|
150
|
-
refs: string[];
|
|
151
|
-
files: CommitFile[];
|
|
152
|
-
totalAdditions: number;
|
|
153
|
-
totalDeletions: number;
|
|
154
|
-
/** Unified patch text for the commit, or `null` when omitted/unavailable. */
|
|
155
|
-
patch: string | null;
|
|
156
|
-
/** `true` when `patch` was clipped to {@link PATCH_CHAR_LIMIT}. */
|
|
157
|
-
truncated: boolean;
|
|
158
|
-
}
|
|
159
|
-
interface ShowArgs {
|
|
160
|
-
/** Commit-ish to inspect (full or short hash). */
|
|
161
|
-
hash: string;
|
|
162
|
-
/** Include the full unified patch (default `true`). */
|
|
163
|
-
patch?: boolean;
|
|
164
|
-
}
|
|
165
|
-
//#endregion
|
|
166
|
-
//#region src/rpc/functions/stage.d.ts
|
|
167
|
-
interface StageArgs {
|
|
168
|
-
/** Paths to stage (`git add`). */
|
|
169
|
-
paths: string[];
|
|
170
|
-
}
|
|
171
|
-
//#endregion
|
|
172
|
-
//#region src/rpc/functions/unstage.d.ts
|
|
173
|
-
interface UnstageArgs {
|
|
174
|
-
/** Paths to unstage (`git restore --staged`). */
|
|
175
|
-
paths: string[];
|
|
176
|
-
}
|
|
177
|
-
//#endregion
|
|
178
2
|
//#region src/index.d.ts
|
|
179
3
|
interface GitDevframeOptions {
|
|
180
4
|
/** Repository directory to inspect. Defaults to the devframe `cwd`. */
|
|
@@ -188,11 +12,6 @@ interface GitDevframeOptions {
|
|
|
188
12
|
distDir?: string;
|
|
189
13
|
/** Preferred dev-server port (default 9710). */
|
|
190
14
|
port?: number;
|
|
191
|
-
/**
|
|
192
|
-
* Enable staging, unstaging, and committing from the UI. Read-only by
|
|
193
|
-
* default; the standalone CLI also accepts a `--write` flag.
|
|
194
|
-
*/
|
|
195
|
-
write?: boolean;
|
|
196
15
|
/**
|
|
197
16
|
* Require the trust handshake on the standalone server. Enabled by
|
|
198
17
|
* default — `--open` embeds the current OTP in the opened URL, so the
|
|
@@ -202,12 +21,15 @@ interface GitDevframeOptions {
|
|
|
202
21
|
auth?: boolean;
|
|
203
22
|
}
|
|
204
23
|
/**
|
|
205
|
-
* Create the Git dashboard devframe.
|
|
206
|
-
*
|
|
24
|
+
* Create the Git dashboard devframe. All git work runs through the
|
|
25
|
+
* `@devframes/service-git` wire service (declared below); the SPA calls its
|
|
26
|
+
* `devframes:service:git:*` RPC directly. Mount it into any host via
|
|
27
|
+
* devframe's adapters, or run it standalone with the bundled CLI
|
|
28
|
+
* (`devframe-git`).
|
|
207
29
|
*
|
|
208
30
|
* @experimental This plugin is experimental and may change without a major
|
|
209
31
|
* version bump until it stabilizes.
|
|
210
32
|
*/
|
|
211
33
|
declare function createGitDevframe(options?: GitDevframeOptions): DevframeDefinition;
|
|
212
34
|
//#endregion
|
|
213
|
-
export {
|
|
35
|
+
export { GitDevframeOptions, createGitDevframe, createGitDevframe as default };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createGitDevframe } from "./src-
|
|
1
|
+
import { t as createGitDevframe } from "./src-E7rY0DjA.mjs";
|
|
2
2
|
export { createGitDevframe, createGitDevframe as default };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { defineDevframe } from "devframe";
|
|
3
|
+
import { resolve } from "pathe";
|
|
4
|
+
//#region package.json
|
|
5
|
+
var name = "@devframes/plugin-git";
|
|
6
|
+
var version = "0.9.3";
|
|
7
|
+
var description = "Devframe plugin that shows a Git dashboard for status, log, branches, and diffs.";
|
|
8
|
+
var homepage = "https://github.com/devframes/devframe#readme";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/index.ts
|
|
11
|
+
const remoteAssets = {
|
|
12
|
+
package: `${name}--assets`,
|
|
13
|
+
version
|
|
14
|
+
};
|
|
15
|
+
const GIT_SERVICE = "@devframes/service-git";
|
|
16
|
+
/**
|
|
17
|
+
* Create the Git dashboard devframe. All git work runs through the
|
|
18
|
+
* `@devframes/service-git` wire service (declared below); the SPA calls its
|
|
19
|
+
* `devframes:service:git:*` RPC directly. Mount it into any host via
|
|
20
|
+
* devframe's adapters, or run it standalone with the bundled CLI
|
|
21
|
+
* (`devframe-git`).
|
|
22
|
+
*
|
|
23
|
+
* @experimental This plugin is experimental and may change without a major
|
|
24
|
+
* version bump until it stabilizes.
|
|
25
|
+
*/
|
|
26
|
+
function createGitDevframe(options = {}) {
|
|
27
|
+
const distDir = options.distDir ?? remoteAssets;
|
|
28
|
+
const cwd = options.repoRoot ? resolve(process.cwd(), options.repoRoot) : void 0;
|
|
29
|
+
return defineDevframe({
|
|
30
|
+
id: "devframes_plugin_git",
|
|
31
|
+
name: "Git",
|
|
32
|
+
version,
|
|
33
|
+
packageName: name,
|
|
34
|
+
importMetaUrl: import.meta.url,
|
|
35
|
+
homepage,
|
|
36
|
+
description,
|
|
37
|
+
icon: "ph:git-branch-duotone",
|
|
38
|
+
basePath: options.basePath,
|
|
39
|
+
cli: {
|
|
40
|
+
command: "devframe-git",
|
|
41
|
+
port: options.port ?? 9710,
|
|
42
|
+
distDir,
|
|
43
|
+
auth: options.auth ?? true
|
|
44
|
+
},
|
|
45
|
+
services: [{
|
|
46
|
+
package: GIT_SERVICE,
|
|
47
|
+
...cwd ? { options: { cwd } } : {}
|
|
48
|
+
}, { package: "@devframes/service-shiki" }],
|
|
49
|
+
rpc: { snapshot: [
|
|
50
|
+
"devframes:service:git:status",
|
|
51
|
+
"devframes:service:git:branches",
|
|
52
|
+
"devframes:service:git:diff",
|
|
53
|
+
{
|
|
54
|
+
method: "devframes:service:git:log",
|
|
55
|
+
inputs: [[{ limit: 200 }]]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
method: "devframes:service:git:show",
|
|
59
|
+
inputs: async (ctx) => {
|
|
60
|
+
const git = ctx.services.get(GIT_SERVICE);
|
|
61
|
+
if (!git) return [];
|
|
62
|
+
const { commits } = await git.log({ limit: 200 });
|
|
63
|
+
return commits.map((commit) => [{
|
|
64
|
+
hash: commit.hash,
|
|
65
|
+
patch: false
|
|
66
|
+
}]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
] },
|
|
70
|
+
setup() {}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { createGitDevframe as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devframes/plugin-git",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.3",
|
|
5
5
|
"description": "Devframe plugin that shows a Git dashboard for status, log, branches, and diffs.",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dist"
|
|
34
34
|
],
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@devframes/plugin-git--assets": "0.9.
|
|
36
|
+
"@devframes/plugin-git--assets": "0.9.3"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@devframes/plugin-git--assets": {
|
|
@@ -43,35 +43,38 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"cac": "^7.0.0",
|
|
45
45
|
"pathe": "^2.0.3",
|
|
46
|
-
"
|
|
46
|
+
"@devframes/service-git": "0.9.3",
|
|
47
|
+
"@devframes/service-shiki": "0.9.3",
|
|
48
|
+
"devframe": "0.9.3"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"@antfu/design": "^0.3.4",
|
|
50
52
|
"@floating-ui/react": "^0.27.20",
|
|
51
53
|
"@iconify-json/catppuccin": "^1.2.17",
|
|
52
|
-
"@pierre/diffs": "^1.2.12",
|
|
53
54
|
"@radix-ui/react-scroll-area": "^1.2.18",
|
|
54
55
|
"@radix-ui/react-slot": "^1.3.3",
|
|
55
|
-
"@storybook/addon-a11y": "^10.5.
|
|
56
|
-
"@storybook/addon-docs": "^10.5.
|
|
57
|
-
"@storybook/react-vite": "^10.5.
|
|
56
|
+
"@storybook/addon-a11y": "^10.5.8",
|
|
57
|
+
"@storybook/addon-docs": "^10.5.8",
|
|
58
|
+
"@storybook/react-vite": "^10.5.8",
|
|
58
59
|
"@types/react": "^19.2.18",
|
|
59
60
|
"@types/react-dom": "^19.2.4",
|
|
60
61
|
"@unocss/postcss": "^66.7.5",
|
|
61
62
|
"@vitejs/plugin-react-oxc": "^0.4.3",
|
|
62
63
|
"clsx": "^2.1.1",
|
|
63
64
|
"colorjs.io": "^0.7.1",
|
|
65
|
+
"diff": "^9.0.0",
|
|
64
66
|
"h3": "^2.0.1-rc.26",
|
|
65
|
-
"next": "^16.3.
|
|
67
|
+
"next": "^16.3.1",
|
|
66
68
|
"react": "^19.2.8",
|
|
67
69
|
"react-dom": "^19.2.8",
|
|
68
|
-
"
|
|
70
|
+
"shiki": "^4.4.3",
|
|
71
|
+
"storybook": "^10.5.8",
|
|
69
72
|
"tailwind-merge": "^3.6.0",
|
|
70
73
|
"tsdown": "^0.22.14",
|
|
71
74
|
"unocss": "^66.7.5",
|
|
72
75
|
"vitest": "^4.1.10",
|
|
73
76
|
"ws": "^8.21.3",
|
|
74
|
-
"@devframes/plugin-git--assets": "0.9.
|
|
77
|
+
"@devframes/plugin-git--assets": "0.9.3"
|
|
75
78
|
},
|
|
76
79
|
"scripts": {
|
|
77
80
|
"build": "tsdown && pnpm run build:spa",
|
package/dist/src-CyRmAQrb.mjs
DELETED
|
@@ -1,933 +0,0 @@
|
|
|
1
|
-
import { defineDevframe, defineRpcFunction } from "devframe";
|
|
2
|
-
import { resolve } from "pathe";
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
|
-
import process from "node:process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { s } from "devframe/utils/simple-schema";
|
|
7
|
-
//#region package.json
|
|
8
|
-
var name = "@devframes/plugin-git";
|
|
9
|
-
var version = "0.9.1";
|
|
10
|
-
var description = "Devframe plugin that shows a Git dashboard for status, log, branches, and diffs.";
|
|
11
|
-
var homepage = "https://github.com/devframes/devframe#readme";
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/node/git.ts
|
|
14
|
-
const execFileAsync = promisify(execFile);
|
|
15
|
-
const MAX_BUFFER = 67108864;
|
|
16
|
-
/**
|
|
17
|
-
* Run a git command in `cwd`. Rejects when git exits non-zero — callers that
|
|
18
|
-
* tolerate failure (e.g. "no upstream configured") should use {@link tryGit}.
|
|
19
|
-
*/
|
|
20
|
-
async function runGit(cwd, args) {
|
|
21
|
-
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
22
|
-
cwd,
|
|
23
|
-
maxBuffer: MAX_BUFFER,
|
|
24
|
-
windowsHide: true,
|
|
25
|
-
env: {
|
|
26
|
-
...process.env,
|
|
27
|
-
GIT_PAGER: "cat",
|
|
28
|
-
GIT_OPTIONAL_LOCKS: "0",
|
|
29
|
-
LC_ALL: "C"
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
return {
|
|
33
|
-
stdout,
|
|
34
|
-
stderr
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/** Run a git command, returning trimmed stdout or `null` when it fails. */
|
|
38
|
-
async function tryGit(cwd, args) {
|
|
39
|
-
try {
|
|
40
|
-
const { stdout } = await runGit(cwd, args);
|
|
41
|
-
return stdout.replace(/\n$/, "");
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Client-supplied revisions must not start with `-`, which Git would parse as
|
|
48
|
-
* an option before it treats the value as a revision.
|
|
49
|
-
*/
|
|
50
|
-
function isSafeRevision(rev) {
|
|
51
|
-
return rev.length > 0 && !rev.startsWith("-");
|
|
52
|
-
}
|
|
53
|
-
/** Resolve the repository root for `cwd`, or `null` when `cwd` is outside a repo. */
|
|
54
|
-
async function resolveRepoRoot(cwd) {
|
|
55
|
-
return tryGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
56
|
-
}
|
|
57
|
-
/** Split git output on a separator, dropping the trailing empty segment. */
|
|
58
|
-
function splitClean(input, separator) {
|
|
59
|
-
return input.split(separator).filter((part) => part.length > 0);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Extract a concise, single-line message from a failed `execFile` error.
|
|
63
|
-
* git writes useful text (e.g. "nothing to commit") to stdout/stderr.
|
|
64
|
-
*/
|
|
65
|
-
function gitErrorMessage(error) {
|
|
66
|
-
const e = error;
|
|
67
|
-
return (e?.stderr || e?.stdout || e?.message || "git command failed").trim().split("\n")[0];
|
|
68
|
-
}
|
|
69
|
-
//#endregion
|
|
70
|
-
//#region src/rpc/context.ts
|
|
71
|
-
const configs = /* @__PURE__ */ new WeakMap();
|
|
72
|
-
const contexts = /* @__PURE__ */ new WeakMap();
|
|
73
|
-
/**
|
|
74
|
-
* Record the working directory for a context. Called from the devframe
|
|
75
|
-
* `setup` before any RPC handler runs, so {@link getGitContext} can honor a
|
|
76
|
-
* `repoRoot` override instead of the raw `ctx.cwd`.
|
|
77
|
-
*/
|
|
78
|
-
function configureGit(ctx, config) {
|
|
79
|
-
configs.set(ctx, config);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Per-`DevframeNodeContext` git state. Each RPC function file pulls its
|
|
83
|
-
* working directory and (memoized) repo-root lookup from here instead of
|
|
84
|
-
* re-resolving on every call.
|
|
85
|
-
*/
|
|
86
|
-
function getGitContext(ctx) {
|
|
87
|
-
let existing = contexts.get(ctx);
|
|
88
|
-
if (existing) return existing;
|
|
89
|
-
const config = configs.get(ctx);
|
|
90
|
-
const cwd = config?.cwd ?? ctx.cwd;
|
|
91
|
-
const write = config?.write ?? false;
|
|
92
|
-
let rootPromise;
|
|
93
|
-
existing = {
|
|
94
|
-
cwd,
|
|
95
|
-
write,
|
|
96
|
-
resolveRoot: () => {
|
|
97
|
-
rootPromise ??= resolveRepoRoot(cwd);
|
|
98
|
-
return rootPromise;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
contexts.set(ctx, existing);
|
|
102
|
-
return existing;
|
|
103
|
-
}
|
|
104
|
-
//#endregion
|
|
105
|
-
//#region src/rpc/functions/branches.ts
|
|
106
|
-
const FORMAT$1 = [
|
|
107
|
-
"%(refname:short)",
|
|
108
|
-
"%(objectname:short)",
|
|
109
|
-
"%(HEAD)",
|
|
110
|
-
"%(upstream:short)",
|
|
111
|
-
"%(upstream:track)",
|
|
112
|
-
"%(contents:subject)"
|
|
113
|
-
].join("");
|
|
114
|
-
function parseTrack(track) {
|
|
115
|
-
if (track.includes("gone")) return {
|
|
116
|
-
ahead: 0,
|
|
117
|
-
behind: 0,
|
|
118
|
-
gone: true
|
|
119
|
-
};
|
|
120
|
-
const ahead = track.match(/ahead (\d+)/);
|
|
121
|
-
const behind = track.match(/behind (\d+)/);
|
|
122
|
-
return {
|
|
123
|
-
ahead: ahead ? Number(ahead[1]) : 0,
|
|
124
|
-
behind: behind ? Number(behind[1]) : 0,
|
|
125
|
-
gone: false
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
const branches = defineRpcFunction({
|
|
129
|
-
name: "devframes:plugin:git:branches",
|
|
130
|
-
type: "query",
|
|
131
|
-
snapshot: true,
|
|
132
|
-
jsonSerializable: true,
|
|
133
|
-
agent: {
|
|
134
|
-
description: "List local and remote branches of the inspected repository with tracking state (ahead/behind, gone upstreams) and the current branch. Safe to call freely.",
|
|
135
|
-
title: "Git branches"
|
|
136
|
-
},
|
|
137
|
-
setup: (ctx) => {
|
|
138
|
-
const git = getGitContext(ctx);
|
|
139
|
-
return { handler: async () => {
|
|
140
|
-
if (!await git.resolveRoot()) return {
|
|
141
|
-
isRepo: false,
|
|
142
|
-
current: null,
|
|
143
|
-
branches: []
|
|
144
|
-
};
|
|
145
|
-
const raw = await tryGit(git.cwd, [
|
|
146
|
-
"for-each-ref",
|
|
147
|
-
`--format=${FORMAT$1}`,
|
|
148
|
-
"refs/heads"
|
|
149
|
-
]);
|
|
150
|
-
if (!raw) return {
|
|
151
|
-
isRepo: true,
|
|
152
|
-
current: null,
|
|
153
|
-
branches: []
|
|
154
|
-
};
|
|
155
|
-
let current = null;
|
|
156
|
-
const branches = splitClean(raw, "\n").map((line) => {
|
|
157
|
-
const [name, sha, head, upstream, track, subject] = line.split("");
|
|
158
|
-
const isCurrent = head === "*";
|
|
159
|
-
if (isCurrent) current = name;
|
|
160
|
-
return {
|
|
161
|
-
name,
|
|
162
|
-
current: isCurrent,
|
|
163
|
-
sha,
|
|
164
|
-
upstream: upstream || null,
|
|
165
|
-
subject: subject ?? "",
|
|
166
|
-
...parseTrack(track ?? "")
|
|
167
|
-
};
|
|
168
|
-
});
|
|
169
|
-
branches.sort((a, b) => Number(b.current) - Number(a.current));
|
|
170
|
-
return {
|
|
171
|
-
isRepo: true,
|
|
172
|
-
current,
|
|
173
|
-
branches
|
|
174
|
-
};
|
|
175
|
-
} };
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
//#endregion
|
|
179
|
-
//#region src/rpc/functions/status.ts
|
|
180
|
-
const EMPTY_STATUS = {
|
|
181
|
-
isRepo: false,
|
|
182
|
-
root: null,
|
|
183
|
-
branch: null,
|
|
184
|
-
detached: false,
|
|
185
|
-
head: null,
|
|
186
|
-
upstream: null,
|
|
187
|
-
ahead: 0,
|
|
188
|
-
behind: 0,
|
|
189
|
-
staged: [],
|
|
190
|
-
unstaged: [],
|
|
191
|
-
untracked: [],
|
|
192
|
-
clean: true,
|
|
193
|
-
canWrite: false
|
|
194
|
-
};
|
|
195
|
-
function mapCode(code) {
|
|
196
|
-
switch (code) {
|
|
197
|
-
case "M": return "modified";
|
|
198
|
-
case "A": return "added";
|
|
199
|
-
case "D": return "deleted";
|
|
200
|
-
case "R": return "renamed";
|
|
201
|
-
case "C": return "copied";
|
|
202
|
-
case "T": return "type-changed";
|
|
203
|
-
case "U": return "unmerged";
|
|
204
|
-
default: return "unknown";
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Parse `git status --porcelain=v2 --branch -z` into a structured snapshot.
|
|
209
|
-
* Records are NUL-separated; rename/copy (type `2`) entries consume an extra
|
|
210
|
-
* token for the original path.
|
|
211
|
-
*/
|
|
212
|
-
function parseStatus(root, raw) {
|
|
213
|
-
const tokens = raw.split("\0");
|
|
214
|
-
const status = {
|
|
215
|
-
...EMPTY_STATUS,
|
|
216
|
-
isRepo: true,
|
|
217
|
-
root,
|
|
218
|
-
staged: [],
|
|
219
|
-
unstaged: [],
|
|
220
|
-
untracked: []
|
|
221
|
-
};
|
|
222
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
223
|
-
const token = tokens[i];
|
|
224
|
-
if (!token) continue;
|
|
225
|
-
if (token.startsWith("# ")) {
|
|
226
|
-
const [, key, ...rest] = token.split(" ");
|
|
227
|
-
const value = rest.join(" ");
|
|
228
|
-
if (key === "branch.head") {
|
|
229
|
-
if (value === "(detached)") {
|
|
230
|
-
status.detached = true;
|
|
231
|
-
status.branch = null;
|
|
232
|
-
} else status.branch = value;
|
|
233
|
-
} else if (key === "branch.oid" && value !== "(initial)") status.head = value.slice(0, 9);
|
|
234
|
-
else if (key === "branch.upstream") status.upstream = value;
|
|
235
|
-
else if (key === "branch.ab") {
|
|
236
|
-
const match = value.match(/\+(\d+)\s+-(\d+)/);
|
|
237
|
-
if (match) {
|
|
238
|
-
status.ahead = Number(match[1]);
|
|
239
|
-
status.behind = Number(match[2]);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
if (token.startsWith("1 ") || token.startsWith("2 ")) {
|
|
245
|
-
const renamed = token.startsWith("2 ");
|
|
246
|
-
const fields = token.split(" ");
|
|
247
|
-
const xy = fields[1];
|
|
248
|
-
const x = xy[0];
|
|
249
|
-
const y = xy[1];
|
|
250
|
-
const path = fields.slice(renamed ? 9 : 8).join(" ");
|
|
251
|
-
const from = renamed ? tokens[++i] : void 0;
|
|
252
|
-
if (x !== ".") status.staged.push(from ? {
|
|
253
|
-
path,
|
|
254
|
-
from,
|
|
255
|
-
status: mapCode(x)
|
|
256
|
-
} : {
|
|
257
|
-
path,
|
|
258
|
-
status: mapCode(x)
|
|
259
|
-
});
|
|
260
|
-
if (y !== ".") status.unstaged.push({
|
|
261
|
-
path,
|
|
262
|
-
status: mapCode(y)
|
|
263
|
-
});
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
if (token.startsWith("u ")) {
|
|
267
|
-
const path = token.split(" ").slice(10).join(" ");
|
|
268
|
-
status.unstaged.push({
|
|
269
|
-
path,
|
|
270
|
-
status: "unmerged"
|
|
271
|
-
});
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (token.startsWith("? ")) status.untracked.push(token.slice(2));
|
|
275
|
-
}
|
|
276
|
-
status.clean = status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0;
|
|
277
|
-
return status;
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Read the working-tree status for a git context. Shared by the `devframes:plugin:git:status`
|
|
281
|
-
* query and the write actions (which return fresh status after mutating).
|
|
282
|
-
*/
|
|
283
|
-
async function readStatus(git) {
|
|
284
|
-
const root = await git.resolveRoot();
|
|
285
|
-
if (!root) return {
|
|
286
|
-
...EMPTY_STATUS,
|
|
287
|
-
canWrite: false
|
|
288
|
-
};
|
|
289
|
-
const { stdout } = await runGit(git.cwd, [
|
|
290
|
-
"status",
|
|
291
|
-
"--porcelain=v2",
|
|
292
|
-
"--branch",
|
|
293
|
-
"-z"
|
|
294
|
-
]);
|
|
295
|
-
const status = parseStatus(root, stdout);
|
|
296
|
-
status.canWrite = git.write;
|
|
297
|
-
return status;
|
|
298
|
-
}
|
|
299
|
-
const status = defineRpcFunction({
|
|
300
|
-
name: "devframes:plugin:git:status",
|
|
301
|
-
type: "query",
|
|
302
|
-
snapshot: true,
|
|
303
|
-
jsonSerializable: true,
|
|
304
|
-
agent: {
|
|
305
|
-
description: "Working-tree status of the inspected repository: current branch, ahead/behind counts, and every staged/unstaged/untracked file. Call this first to orient before reading diffs or history. Safe to call freely.",
|
|
306
|
-
title: "Git status"
|
|
307
|
-
},
|
|
308
|
-
setup: (ctx) => {
|
|
309
|
-
const git = getGitContext(ctx);
|
|
310
|
-
return { handler: () => readStatus(git) };
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
//#endregion
|
|
314
|
-
//#region src/rpc/functions/commit.ts
|
|
315
|
-
const commit = defineRpcFunction({
|
|
316
|
-
name: "devframes:plugin:git:commit",
|
|
317
|
-
type: "action",
|
|
318
|
-
jsonSerializable: true,
|
|
319
|
-
setup: (ctx) => {
|
|
320
|
-
const git = getGitContext(ctx);
|
|
321
|
-
return { handler: async (args) => {
|
|
322
|
-
const message = (args?.message ?? "").trim();
|
|
323
|
-
if (!await git.resolveRoot()) return {
|
|
324
|
-
ok: false,
|
|
325
|
-
hash: null,
|
|
326
|
-
message: "Not a git repository.",
|
|
327
|
-
status: await readStatus(git)
|
|
328
|
-
};
|
|
329
|
-
if (!message) return {
|
|
330
|
-
ok: false,
|
|
331
|
-
hash: null,
|
|
332
|
-
message: "Commit message is required.",
|
|
333
|
-
status: await readStatus(git)
|
|
334
|
-
};
|
|
335
|
-
try {
|
|
336
|
-
await runGit(git.cwd, [
|
|
337
|
-
"commit",
|
|
338
|
-
"-m",
|
|
339
|
-
message
|
|
340
|
-
]);
|
|
341
|
-
return {
|
|
342
|
-
ok: true,
|
|
343
|
-
hash: await tryGit(git.cwd, [
|
|
344
|
-
"rev-parse",
|
|
345
|
-
"--short",
|
|
346
|
-
"HEAD"
|
|
347
|
-
]),
|
|
348
|
-
message: "Committed.",
|
|
349
|
-
status: await readStatus(git)
|
|
350
|
-
};
|
|
351
|
-
} catch (error) {
|
|
352
|
-
return {
|
|
353
|
-
ok: false,
|
|
354
|
-
hash: null,
|
|
355
|
-
message: gitErrorMessage(error),
|
|
356
|
-
status: await readStatus(git)
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
} };
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
//#endregion
|
|
363
|
-
//#region src/rpc/functions/diff.ts
|
|
364
|
-
/** Hard cap on the returned patch text to keep payloads bounded. */
|
|
365
|
-
const PATCH_CHAR_LIMIT$1 = 2e5;
|
|
366
|
-
const diffFileSchema = s.object({
|
|
367
|
-
path: s.string(),
|
|
368
|
-
additions: s.number(),
|
|
369
|
-
deletions: s.number(),
|
|
370
|
-
binary: s.boolean()
|
|
371
|
-
});
|
|
372
|
-
const gitDiffSchema = s.object({
|
|
373
|
-
isRepo: s.boolean(),
|
|
374
|
-
staged: s.boolean(),
|
|
375
|
-
path: s.nullable(s.string()),
|
|
376
|
-
files: s.array(diffFileSchema),
|
|
377
|
-
totalAdditions: s.number(),
|
|
378
|
-
totalDeletions: s.number(),
|
|
379
|
-
patch: s.nullable(s.string()),
|
|
380
|
-
truncated: s.boolean()
|
|
381
|
-
});
|
|
382
|
-
function parseNumstat$1(raw) {
|
|
383
|
-
return splitClean(raw, "\n").map((line) => {
|
|
384
|
-
const [add, del, ...rest] = line.split(" ");
|
|
385
|
-
const binary = add === "-" || del === "-";
|
|
386
|
-
return {
|
|
387
|
-
path: rest.join(" "),
|
|
388
|
-
additions: binary ? 0 : Number(add),
|
|
389
|
-
deletions: binary ? 0 : Number(del),
|
|
390
|
-
binary
|
|
391
|
-
};
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
const diff = defineRpcFunction({
|
|
395
|
-
name: "devframes:plugin:git:diff",
|
|
396
|
-
type: "query",
|
|
397
|
-
snapshot: true,
|
|
398
|
-
jsonSerializable: true,
|
|
399
|
-
args: [s.object({
|
|
400
|
-
path: s.optional(s.string()),
|
|
401
|
-
staged: s.optional(s.boolean())
|
|
402
|
-
})],
|
|
403
|
-
returns: gitDiffSchema,
|
|
404
|
-
agent: {
|
|
405
|
-
description: "Unified diff of uncommitted changes in the inspected repository — the working tree by default, the index with staged: true, one file with path. Call before summarizing or reviewing in-progress work. Safe to call freely.",
|
|
406
|
-
title: "Git diff"
|
|
407
|
-
},
|
|
408
|
-
setup: (ctx) => {
|
|
409
|
-
const git = getGitContext(ctx);
|
|
410
|
-
return { handler: async (args = {}) => {
|
|
411
|
-
const { path, staged = false } = args;
|
|
412
|
-
if (!await git.resolveRoot()) return {
|
|
413
|
-
isRepo: false,
|
|
414
|
-
staged,
|
|
415
|
-
path: path ?? null,
|
|
416
|
-
files: [],
|
|
417
|
-
totalAdditions: 0,
|
|
418
|
-
totalDeletions: 0,
|
|
419
|
-
patch: null,
|
|
420
|
-
truncated: false
|
|
421
|
-
};
|
|
422
|
-
const base = staged ? ["diff", "--cached"] : ["diff"];
|
|
423
|
-
const scope = path ? ["--", path] : [];
|
|
424
|
-
const numstatRaw = await tryGit(git.cwd, [
|
|
425
|
-
...base,
|
|
426
|
-
"--numstat",
|
|
427
|
-
...scope
|
|
428
|
-
]);
|
|
429
|
-
const files = numstatRaw ? parseNumstat$1(numstatRaw) : [];
|
|
430
|
-
const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
|
|
431
|
-
const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
|
432
|
-
let patch = null;
|
|
433
|
-
let truncated = false;
|
|
434
|
-
if (path) {
|
|
435
|
-
const { stdout } = await runGit(git.cwd, [...base, ...scope]);
|
|
436
|
-
if (stdout.length > PATCH_CHAR_LIMIT$1) {
|
|
437
|
-
patch = stdout.slice(0, PATCH_CHAR_LIMIT$1);
|
|
438
|
-
truncated = true;
|
|
439
|
-
} else patch = stdout;
|
|
440
|
-
}
|
|
441
|
-
return {
|
|
442
|
-
isRepo: true,
|
|
443
|
-
staged,
|
|
444
|
-
path: path ?? null,
|
|
445
|
-
files,
|
|
446
|
-
totalAdditions,
|
|
447
|
-
totalDeletions,
|
|
448
|
-
patch,
|
|
449
|
-
truncated
|
|
450
|
-
};
|
|
451
|
-
} };
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
//#endregion
|
|
455
|
-
//#region src/rpc/functions/log.ts
|
|
456
|
-
const commitSchema = s.object({
|
|
457
|
-
hash: s.string(),
|
|
458
|
-
shortHash: s.string(),
|
|
459
|
-
author: s.string(),
|
|
460
|
-
email: s.string(),
|
|
461
|
-
date: s.number(),
|
|
462
|
-
subject: s.string(),
|
|
463
|
-
body: s.string(),
|
|
464
|
-
refs: s.array(s.string()),
|
|
465
|
-
parents: s.array(s.string())
|
|
466
|
-
});
|
|
467
|
-
const gitLogSchema = s.object({
|
|
468
|
-
isRepo: s.boolean(),
|
|
469
|
-
commits: s.array(commitSchema),
|
|
470
|
-
limit: s.number(),
|
|
471
|
-
skip: s.number(),
|
|
472
|
-
hasMore: s.boolean()
|
|
473
|
-
});
|
|
474
|
-
const FORMAT = [
|
|
475
|
-
"%H",
|
|
476
|
-
"%h",
|
|
477
|
-
"%P",
|
|
478
|
-
"%an",
|
|
479
|
-
"%ae",
|
|
480
|
-
"%aI",
|
|
481
|
-
"%D",
|
|
482
|
-
"%s",
|
|
483
|
-
"%b"
|
|
484
|
-
].join("") + "";
|
|
485
|
-
function clamp(value, min, max) {
|
|
486
|
-
return Math.min(Math.max(value, min), max);
|
|
487
|
-
}
|
|
488
|
-
function parseLog(raw) {
|
|
489
|
-
return splitClean(raw, "").map((record) => {
|
|
490
|
-
const [hash, shortHash, parents, author, email, isoDate, refs, subject, body] = record.replace(/^\n/, "").split("");
|
|
491
|
-
return {
|
|
492
|
-
hash,
|
|
493
|
-
shortHash,
|
|
494
|
-
author,
|
|
495
|
-
email,
|
|
496
|
-
date: Date.parse(isoDate),
|
|
497
|
-
subject,
|
|
498
|
-
body: (body ?? "").trim(),
|
|
499
|
-
refs: refs ? refs.split(", ").map((r) => r.trim()).filter(Boolean) : [],
|
|
500
|
-
parents: parents ? parents.split(" ").filter(Boolean) : []
|
|
501
|
-
};
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
const SNAPSHOT_LIMIT$1 = 200;
|
|
505
|
-
const log = defineRpcFunction({
|
|
506
|
-
name: "devframes:plugin:git:log",
|
|
507
|
-
type: "query",
|
|
508
|
-
jsonSerializable: true,
|
|
509
|
-
args: [s.object({
|
|
510
|
-
limit: s.optional(s.number()),
|
|
511
|
-
skip: s.optional(s.number()),
|
|
512
|
-
ref: s.optional(s.string())
|
|
513
|
-
})],
|
|
514
|
-
returns: gitLogSchema,
|
|
515
|
-
agent: {
|
|
516
|
-
description: "Commit history of the inspected repository, newest first. Paginate with limit (1-200, default 30) and skip; pass ref to read another branch. Call before reasoning about recent changes. Safe to call freely.",
|
|
517
|
-
title: "Git log"
|
|
518
|
-
},
|
|
519
|
-
dump: async (_ctx, handler) => {
|
|
520
|
-
const baked = {
|
|
521
|
-
...await handler({
|
|
522
|
-
limit: SNAPSHOT_LIMIT$1,
|
|
523
|
-
skip: 0
|
|
524
|
-
}),
|
|
525
|
-
hasMore: false
|
|
526
|
-
};
|
|
527
|
-
return {
|
|
528
|
-
records: [{
|
|
529
|
-
inputs: [],
|
|
530
|
-
output: baked
|
|
531
|
-
}],
|
|
532
|
-
fallback: baked
|
|
533
|
-
};
|
|
534
|
-
},
|
|
535
|
-
setup: (ctx) => {
|
|
536
|
-
const git = getGitContext(ctx);
|
|
537
|
-
return { handler: async (args = {}) => {
|
|
538
|
-
const limit = clamp(Math.trunc(args.limit ?? 30), 1, 200);
|
|
539
|
-
const skip = Math.max(0, Math.trunc(args.skip ?? 0));
|
|
540
|
-
const ref = args.ref?.trim() || void 0;
|
|
541
|
-
if (!await git.resolveRoot()) return {
|
|
542
|
-
isRepo: false,
|
|
543
|
-
commits: [],
|
|
544
|
-
limit,
|
|
545
|
-
skip,
|
|
546
|
-
hasMore: false
|
|
547
|
-
};
|
|
548
|
-
const command = [
|
|
549
|
-
"log",
|
|
550
|
-
"--topo-order",
|
|
551
|
-
`--max-count=${limit}`,
|
|
552
|
-
`--skip=${skip}`,
|
|
553
|
-
`--pretty=format:${FORMAT}`
|
|
554
|
-
];
|
|
555
|
-
if (ref) {
|
|
556
|
-
if (!isSafeRevision(ref)) return {
|
|
557
|
-
isRepo: true,
|
|
558
|
-
commits: [],
|
|
559
|
-
limit,
|
|
560
|
-
skip,
|
|
561
|
-
hasMore: false
|
|
562
|
-
};
|
|
563
|
-
command.push("--end-of-options", ref);
|
|
564
|
-
}
|
|
565
|
-
const raw = await tryGit(git.cwd, command);
|
|
566
|
-
const commits = raw ? parseLog(raw) : [];
|
|
567
|
-
return {
|
|
568
|
-
isRepo: true,
|
|
569
|
-
commits,
|
|
570
|
-
limit,
|
|
571
|
-
skip,
|
|
572
|
-
hasMore: commits.length === limit
|
|
573
|
-
};
|
|
574
|
-
} };
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
//#endregion
|
|
578
|
-
//#region src/rpc/functions/show.ts
|
|
579
|
-
/** Hard cap on the returned patch text to keep payloads bounded. */
|
|
580
|
-
const PATCH_CHAR_LIMIT = 2e5;
|
|
581
|
-
/** Matches the window `devframes:plugin:git:log` bakes, so any visible commit has a snapshot. */
|
|
582
|
-
const SNAPSHOT_LIMIT = 200;
|
|
583
|
-
/** Read-only git detail work can run in parallel without overwhelming builds. */
|
|
584
|
-
const DUMP_CONCURRENCY = 8;
|
|
585
|
-
const fileStatusCodeSchema = s.picklist([
|
|
586
|
-
"modified",
|
|
587
|
-
"added",
|
|
588
|
-
"deleted",
|
|
589
|
-
"renamed",
|
|
590
|
-
"copied",
|
|
591
|
-
"type-changed",
|
|
592
|
-
"unmerged",
|
|
593
|
-
"unknown"
|
|
594
|
-
]);
|
|
595
|
-
const commitFileSchema = s.object({
|
|
596
|
-
path: s.string(),
|
|
597
|
-
additions: s.number(),
|
|
598
|
-
deletions: s.number(),
|
|
599
|
-
binary: s.boolean(),
|
|
600
|
-
status: fileStatusCodeSchema
|
|
601
|
-
});
|
|
602
|
-
const commitDetailSchema = s.object({
|
|
603
|
-
isRepo: s.boolean(),
|
|
604
|
-
found: s.boolean(),
|
|
605
|
-
hash: s.string(),
|
|
606
|
-
shortHash: s.string(),
|
|
607
|
-
author: s.string(),
|
|
608
|
-
email: s.string(),
|
|
609
|
-
date: s.number(),
|
|
610
|
-
committer: s.string(),
|
|
611
|
-
committerEmail: s.string(),
|
|
612
|
-
commitDate: s.number(),
|
|
613
|
-
subject: s.string(),
|
|
614
|
-
body: s.string(),
|
|
615
|
-
parents: s.array(s.string()),
|
|
616
|
-
refs: s.array(s.string()),
|
|
617
|
-
files: s.array(commitFileSchema),
|
|
618
|
-
totalAdditions: s.number(),
|
|
619
|
-
totalDeletions: s.number(),
|
|
620
|
-
patch: s.nullable(s.string()),
|
|
621
|
-
truncated: s.boolean()
|
|
622
|
-
});
|
|
623
|
-
const EMPTY_DETAIL = {
|
|
624
|
-
isRepo: false,
|
|
625
|
-
found: false,
|
|
626
|
-
hash: "",
|
|
627
|
-
shortHash: "",
|
|
628
|
-
author: "",
|
|
629
|
-
email: "",
|
|
630
|
-
date: 0,
|
|
631
|
-
committer: "",
|
|
632
|
-
committerEmail: "",
|
|
633
|
-
commitDate: 0,
|
|
634
|
-
subject: "",
|
|
635
|
-
body: "",
|
|
636
|
-
parents: [],
|
|
637
|
-
refs: [],
|
|
638
|
-
files: [],
|
|
639
|
-
totalAdditions: 0,
|
|
640
|
-
totalDeletions: 0,
|
|
641
|
-
patch: null,
|
|
642
|
-
truncated: false
|
|
643
|
-
};
|
|
644
|
-
const SHOW_FORMAT = [
|
|
645
|
-
"%H",
|
|
646
|
-
"%h",
|
|
647
|
-
"%P",
|
|
648
|
-
"%an",
|
|
649
|
-
"%ae",
|
|
650
|
-
"%aI",
|
|
651
|
-
"%cn",
|
|
652
|
-
"%ce",
|
|
653
|
-
"%cI",
|
|
654
|
-
"%D",
|
|
655
|
-
"%s",
|
|
656
|
-
"%b"
|
|
657
|
-
].join("");
|
|
658
|
-
function mapStatusCode(code) {
|
|
659
|
-
switch (code[0]) {
|
|
660
|
-
case "M": return "modified";
|
|
661
|
-
case "A": return "added";
|
|
662
|
-
case "D": return "deleted";
|
|
663
|
-
case "R": return "renamed";
|
|
664
|
-
case "C": return "copied";
|
|
665
|
-
case "T": return "type-changed";
|
|
666
|
-
case "U": return "unmerged";
|
|
667
|
-
default: return "unknown";
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Parse `git diff-tree --name-status` into a `path → status` map. Rename/copy
|
|
672
|
-
* rows carry a similarity score and both old + new paths; the new path (last
|
|
673
|
-
* field) keys the map so it aligns with the numstat entry.
|
|
674
|
-
*/
|
|
675
|
-
function parseNameStatus(raw) {
|
|
676
|
-
const map = /* @__PURE__ */ new Map();
|
|
677
|
-
for (const line of splitClean(raw, "\n")) {
|
|
678
|
-
const [code, ...paths] = line.split(" ");
|
|
679
|
-
const path = paths[paths.length - 1];
|
|
680
|
-
if (path) map.set(path, mapStatusCode(code));
|
|
681
|
-
}
|
|
682
|
-
return map;
|
|
683
|
-
}
|
|
684
|
-
function parseNumstat(raw, status) {
|
|
685
|
-
return splitClean(raw, "\n").map((line) => {
|
|
686
|
-
const [add, del, ...rest] = line.split(" ");
|
|
687
|
-
const binary = add === "-" || del === "-";
|
|
688
|
-
const path = rest.join(" ");
|
|
689
|
-
return {
|
|
690
|
-
path,
|
|
691
|
-
additions: binary ? 0 : Number(add),
|
|
692
|
-
deletions: binary ? 0 : Number(del),
|
|
693
|
-
binary,
|
|
694
|
-
status: status.get(path) ?? "modified"
|
|
695
|
-
};
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
async function readCommit(git, hash, includePatch) {
|
|
699
|
-
if (!isSafeRevision(hash)) return {
|
|
700
|
-
...EMPTY_DETAIL,
|
|
701
|
-
isRepo: true
|
|
702
|
-
};
|
|
703
|
-
const meta = await tryGit(git.cwd, [
|
|
704
|
-
"show",
|
|
705
|
-
"-s",
|
|
706
|
-
`--format=${SHOW_FORMAT}`,
|
|
707
|
-
"--end-of-options",
|
|
708
|
-
hash
|
|
709
|
-
]);
|
|
710
|
-
if (meta == null) return {
|
|
711
|
-
...EMPTY_DETAIL,
|
|
712
|
-
isRepo: true
|
|
713
|
-
};
|
|
714
|
-
const [fullHash, shortHash, parents, author, email, authorDate, committer, committerEmail, committerDate, refs, subject, body] = meta.split("");
|
|
715
|
-
const numstat = await tryGit(git.cwd, [
|
|
716
|
-
"diff-tree",
|
|
717
|
-
"--no-commit-id",
|
|
718
|
-
"--numstat",
|
|
719
|
-
"-r",
|
|
720
|
-
"--root",
|
|
721
|
-
"--end-of-options",
|
|
722
|
-
hash
|
|
723
|
-
]);
|
|
724
|
-
const nameStatusRaw = await tryGit(git.cwd, [
|
|
725
|
-
"diff-tree",
|
|
726
|
-
"--no-commit-id",
|
|
727
|
-
"--name-status",
|
|
728
|
-
"-r",
|
|
729
|
-
"--root",
|
|
730
|
-
"--end-of-options",
|
|
731
|
-
hash
|
|
732
|
-
]);
|
|
733
|
-
const files = numstat ? parseNumstat(numstat, nameStatusRaw ? parseNameStatus(nameStatusRaw) : /* @__PURE__ */ new Map()) : [];
|
|
734
|
-
const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
|
|
735
|
-
const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
|
736
|
-
let patch = null;
|
|
737
|
-
let truncated = false;
|
|
738
|
-
if (includePatch) {
|
|
739
|
-
const raw = await tryGit(git.cwd, [
|
|
740
|
-
"diff-tree",
|
|
741
|
-
"-p",
|
|
742
|
-
"--no-commit-id",
|
|
743
|
-
"-r",
|
|
744
|
-
"--root",
|
|
745
|
-
"--end-of-options",
|
|
746
|
-
hash
|
|
747
|
-
]);
|
|
748
|
-
if (raw != null) {
|
|
749
|
-
if (raw.length > PATCH_CHAR_LIMIT) {
|
|
750
|
-
patch = raw.slice(0, PATCH_CHAR_LIMIT);
|
|
751
|
-
truncated = true;
|
|
752
|
-
} else patch = raw;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
return {
|
|
756
|
-
isRepo: true,
|
|
757
|
-
found: true,
|
|
758
|
-
hash: fullHash,
|
|
759
|
-
shortHash,
|
|
760
|
-
author,
|
|
761
|
-
email,
|
|
762
|
-
date: Date.parse(authorDate),
|
|
763
|
-
committer,
|
|
764
|
-
committerEmail,
|
|
765
|
-
commitDate: Date.parse(committerDate),
|
|
766
|
-
subject,
|
|
767
|
-
body: (body ?? "").trim(),
|
|
768
|
-
parents: parents ? parents.split(" ").filter(Boolean) : [],
|
|
769
|
-
refs: refs ? refs.split(", ").map((r) => r.trim()).filter(Boolean) : [],
|
|
770
|
-
files,
|
|
771
|
-
totalAdditions,
|
|
772
|
-
totalDeletions,
|
|
773
|
-
patch,
|
|
774
|
-
truncated
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
const show = defineRpcFunction({
|
|
778
|
-
name: "devframes:plugin:git:show",
|
|
779
|
-
type: "query",
|
|
780
|
-
jsonSerializable: true,
|
|
781
|
-
args: [s.object({
|
|
782
|
-
hash: s.string(),
|
|
783
|
-
patch: s.optional(s.boolean())
|
|
784
|
-
})],
|
|
785
|
-
returns: commitDetailSchema,
|
|
786
|
-
agent: {
|
|
787
|
-
description: "Full detail of one commit by hash (from the git log tool): metadata, changed files, and the unified patch (pass patch: false to skip it for large commits). Safe to call freely.",
|
|
788
|
-
title: "Git show"
|
|
789
|
-
},
|
|
790
|
-
dump: async (ctx, _handler) => {
|
|
791
|
-
const git = getGitContext(ctx);
|
|
792
|
-
if (!await git.resolveRoot()) return {
|
|
793
|
-
records: [],
|
|
794
|
-
fallback: EMPTY_DETAIL
|
|
795
|
-
};
|
|
796
|
-
const raw = await tryGit(git.cwd, [
|
|
797
|
-
"log",
|
|
798
|
-
"--topo-order",
|
|
799
|
-
`--max-count=${SNAPSHOT_LIMIT}`,
|
|
800
|
-
"--pretty=format:%H"
|
|
801
|
-
]);
|
|
802
|
-
const hashes = raw ? raw.split("\n").filter(Boolean) : [];
|
|
803
|
-
const records = [];
|
|
804
|
-
for (let i = 0; i < hashes.length; i += DUMP_CONCURRENCY) {
|
|
805
|
-
const batch = hashes.slice(i, i + DUMP_CONCURRENCY);
|
|
806
|
-
const outputs = await Promise.all(batch.map((hash) => readCommit(git, hash, false)));
|
|
807
|
-
batch.forEach((hash, index) => {
|
|
808
|
-
records.push({
|
|
809
|
-
inputs: [{ hash }],
|
|
810
|
-
output: outputs[index]
|
|
811
|
-
});
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
return {
|
|
815
|
-
records,
|
|
816
|
-
fallback: records[0]?.output ?? {
|
|
817
|
-
...EMPTY_DETAIL,
|
|
818
|
-
isRepo: true
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
},
|
|
822
|
-
setup: (ctx) => {
|
|
823
|
-
const git = getGitContext(ctx);
|
|
824
|
-
return { handler: async (args) => {
|
|
825
|
-
const hash = (args?.hash ?? "").trim();
|
|
826
|
-
const includePatch = args?.patch ?? true;
|
|
827
|
-
if (!await git.resolveRoot() || !hash) return EMPTY_DETAIL;
|
|
828
|
-
return readCommit(git, hash, includePatch);
|
|
829
|
-
} };
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
//#endregion
|
|
833
|
-
//#region src/rpc/functions/stage.ts
|
|
834
|
-
const stage = defineRpcFunction({
|
|
835
|
-
name: "devframes:plugin:git:stage",
|
|
836
|
-
type: "action",
|
|
837
|
-
jsonSerializable: true,
|
|
838
|
-
setup: (ctx) => {
|
|
839
|
-
const git = getGitContext(ctx);
|
|
840
|
-
return { handler: async (args) => {
|
|
841
|
-
const paths = args?.paths ?? [];
|
|
842
|
-
if (await git.resolveRoot() && paths.length > 0) await runGit(git.cwd, [
|
|
843
|
-
"add",
|
|
844
|
-
"--",
|
|
845
|
-
...paths
|
|
846
|
-
]);
|
|
847
|
-
return readStatus(git);
|
|
848
|
-
} };
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
//#endregion
|
|
852
|
-
//#region src/rpc/functions/unstage.ts
|
|
853
|
-
const unstage = defineRpcFunction({
|
|
854
|
-
name: "devframes:plugin:git:unstage",
|
|
855
|
-
type: "action",
|
|
856
|
-
jsonSerializable: true,
|
|
857
|
-
setup: (ctx) => {
|
|
858
|
-
const git = getGitContext(ctx);
|
|
859
|
-
return { handler: async (args) => {
|
|
860
|
-
const paths = args?.paths ?? [];
|
|
861
|
-
if (await git.resolveRoot() && paths.length > 0) await runGit(git.cwd, [
|
|
862
|
-
"restore",
|
|
863
|
-
"--staged",
|
|
864
|
-
"--",
|
|
865
|
-
...paths
|
|
866
|
-
]);
|
|
867
|
-
return readStatus(git);
|
|
868
|
-
} };
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
//#endregion
|
|
872
|
-
//#region src/rpc/index.ts
|
|
873
|
-
/** Read-only RPC — always registered. */
|
|
874
|
-
const readFunctions = [
|
|
875
|
-
status,
|
|
876
|
-
log,
|
|
877
|
-
show,
|
|
878
|
-
branches,
|
|
879
|
-
diff
|
|
880
|
-
];
|
|
881
|
-
/** Mutating RPC — registered only when write actions are enabled. */
|
|
882
|
-
const writeFunctions = [
|
|
883
|
-
stage,
|
|
884
|
-
unstage,
|
|
885
|
-
commit
|
|
886
|
-
];
|
|
887
|
-
//#endregion
|
|
888
|
-
//#region src/index.ts
|
|
889
|
-
const remoteAssets = {
|
|
890
|
-
package: `${name}--assets`,
|
|
891
|
-
version,
|
|
892
|
-
resolveFrom: import.meta.url
|
|
893
|
-
};
|
|
894
|
-
/**
|
|
895
|
-
* Create the Git dashboard devframe. Mount it into any host via devframe's
|
|
896
|
-
* adapters, or run it standalone with the bundled CLI (`devframe-git`).
|
|
897
|
-
*
|
|
898
|
-
* @experimental This plugin is experimental and may change without a major
|
|
899
|
-
* version bump until it stabilizes.
|
|
900
|
-
*/
|
|
901
|
-
function createGitDevframe(options = {}) {
|
|
902
|
-
const distDir = options.distDir ?? remoteAssets;
|
|
903
|
-
return defineDevframe({
|
|
904
|
-
id: "devframes_plugin_git",
|
|
905
|
-
name: "Git",
|
|
906
|
-
version,
|
|
907
|
-
packageName: name,
|
|
908
|
-
homepage,
|
|
909
|
-
description,
|
|
910
|
-
icon: "ph:git-branch-duotone",
|
|
911
|
-
basePath: options.basePath,
|
|
912
|
-
cli: {
|
|
913
|
-
command: "devframe-git",
|
|
914
|
-
port: options.port ?? 9710,
|
|
915
|
-
distDir,
|
|
916
|
-
auth: options.auth ?? true,
|
|
917
|
-
configure(cli) {
|
|
918
|
-
cli.option("--write", "Enable staging, unstaging, and committing from the UI");
|
|
919
|
-
}
|
|
920
|
-
},
|
|
921
|
-
setup(ctx, info) {
|
|
922
|
-
const write = options.write ?? info?.flags?.write === true;
|
|
923
|
-
configureGit(ctx, {
|
|
924
|
-
cwd: options.repoRoot ? resolve(options.repoRoot) : ctx.cwd,
|
|
925
|
-
write
|
|
926
|
-
});
|
|
927
|
-
for (const fn of readFunctions) ctx.rpc.register(fn);
|
|
928
|
-
if (write) for (const fn of writeFunctions) ctx.rpc.register(fn);
|
|
929
|
-
}
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
//#endregion
|
|
933
|
-
export { createGitDevframe as t };
|