@devframes/service-git 0.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.
- package/LICENSE.md +21 -0
- package/dist/index.d.mts +214 -0
- package/dist/index.mjs +764 -0
- package/package.json +45 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-PRESENT Anthony Fu <https://github.com/antfu>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { DevframeServiceDefinition } from "devframe/types";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
type FileStatusCode = 'modified' | 'added' | 'deleted' | 'renamed' | 'copied' | 'type-changed' | 'unmerged' | 'unknown';
|
|
4
|
+
interface StatusFileEntry {
|
|
5
|
+
path: string;
|
|
6
|
+
/** Previous path, present for renames and copies. */
|
|
7
|
+
from?: string;
|
|
8
|
+
status: FileStatusCode;
|
|
9
|
+
}
|
|
10
|
+
interface GitStatus {
|
|
11
|
+
/** `false` when the working directory is not inside a git repository. */
|
|
12
|
+
isRepo: boolean;
|
|
13
|
+
root: string | null;
|
|
14
|
+
/** Current branch name, or `null` when HEAD is detached. */
|
|
15
|
+
branch: string | null;
|
|
16
|
+
detached: boolean;
|
|
17
|
+
/** Short HEAD object name. */
|
|
18
|
+
head: string | null;
|
|
19
|
+
upstream: string | null;
|
|
20
|
+
ahead: number;
|
|
21
|
+
behind: number;
|
|
22
|
+
staged: StatusFileEntry[];
|
|
23
|
+
unstaged: StatusFileEntry[];
|
|
24
|
+
untracked: string[];
|
|
25
|
+
/** `true` when there are no staged, unstaged, or untracked changes. */
|
|
26
|
+
clean: boolean;
|
|
27
|
+
/** `true` when stage / unstage / commit actions are available. */
|
|
28
|
+
canWrite: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface Commit {
|
|
31
|
+
hash: string;
|
|
32
|
+
shortHash: string;
|
|
33
|
+
author: string;
|
|
34
|
+
email: string;
|
|
35
|
+
/** Author date as epoch milliseconds. */
|
|
36
|
+
date: number;
|
|
37
|
+
subject: string;
|
|
38
|
+
body: string;
|
|
39
|
+
/** Ref names pointing at this commit (branches, tags, HEAD). */
|
|
40
|
+
refs: string[];
|
|
41
|
+
/** Full parent hashes — drives the commit graph. */
|
|
42
|
+
parents: string[];
|
|
43
|
+
}
|
|
44
|
+
interface GitLog {
|
|
45
|
+
isRepo: boolean;
|
|
46
|
+
commits: Commit[];
|
|
47
|
+
limit: number;
|
|
48
|
+
skip: number;
|
|
49
|
+
/** `true` when the page filled to `limit`, hinting at further history. */
|
|
50
|
+
hasMore: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface LogArgs {
|
|
53
|
+
/** Number of commits to return (clamped to 1–200, default 30). */
|
|
54
|
+
limit?: number;
|
|
55
|
+
/** Commits to skip from the tip, for pagination (default 0). */
|
|
56
|
+
skip?: number;
|
|
57
|
+
/** Optional ref/branch to read history from (default: current HEAD). */
|
|
58
|
+
ref?: string;
|
|
59
|
+
}
|
|
60
|
+
interface Branch {
|
|
61
|
+
name: string;
|
|
62
|
+
current: boolean;
|
|
63
|
+
sha: string;
|
|
64
|
+
upstream: string | null;
|
|
65
|
+
subject: string;
|
|
66
|
+
ahead: number;
|
|
67
|
+
behind: number;
|
|
68
|
+
/** `true` when the upstream branch no longer exists. */
|
|
69
|
+
gone: boolean;
|
|
70
|
+
}
|
|
71
|
+
interface GitBranches {
|
|
72
|
+
isRepo: boolean;
|
|
73
|
+
current: string | null;
|
|
74
|
+
branches: Branch[];
|
|
75
|
+
}
|
|
76
|
+
interface DiffFile {
|
|
77
|
+
path: string;
|
|
78
|
+
additions: number;
|
|
79
|
+
deletions: number;
|
|
80
|
+
binary: boolean;
|
|
81
|
+
}
|
|
82
|
+
interface GitDiff {
|
|
83
|
+
isRepo: boolean;
|
|
84
|
+
staged: boolean;
|
|
85
|
+
path: string | null;
|
|
86
|
+
files: DiffFile[];
|
|
87
|
+
totalAdditions: number;
|
|
88
|
+
totalDeletions: number;
|
|
89
|
+
/** Unified patch text — populated when `path` targets a single file. */
|
|
90
|
+
patch: string | null;
|
|
91
|
+
/** `true` when `patch` was clipped to the internal char limit. */
|
|
92
|
+
truncated: boolean;
|
|
93
|
+
}
|
|
94
|
+
interface DiffArgs {
|
|
95
|
+
/** Limit the diff to a single path; omit for the whole tree. */
|
|
96
|
+
path?: string;
|
|
97
|
+
/** Diff the index against HEAD instead of the working tree. */
|
|
98
|
+
staged?: boolean;
|
|
99
|
+
}
|
|
100
|
+
interface CommitFile {
|
|
101
|
+
path: string;
|
|
102
|
+
additions: number;
|
|
103
|
+
deletions: number;
|
|
104
|
+
binary: boolean;
|
|
105
|
+
/** Change kind relative to the parent (add / modify / delete / rename …). */
|
|
106
|
+
status: FileStatusCode;
|
|
107
|
+
}
|
|
108
|
+
interface CommitDetail {
|
|
109
|
+
/** `false` when the working directory is not inside a git repository. */
|
|
110
|
+
isRepo: boolean;
|
|
111
|
+
/** `false` when the hash does not resolve to a commit. */
|
|
112
|
+
found: boolean;
|
|
113
|
+
hash: string;
|
|
114
|
+
shortHash: string;
|
|
115
|
+
author: string;
|
|
116
|
+
email: string;
|
|
117
|
+
/** Author date as epoch milliseconds. */
|
|
118
|
+
date: number;
|
|
119
|
+
committer: string;
|
|
120
|
+
committerEmail: string;
|
|
121
|
+
/** Commit date as epoch milliseconds. */
|
|
122
|
+
commitDate: number;
|
|
123
|
+
subject: string;
|
|
124
|
+
body: string;
|
|
125
|
+
parents: string[];
|
|
126
|
+
refs: string[];
|
|
127
|
+
files: CommitFile[];
|
|
128
|
+
totalAdditions: number;
|
|
129
|
+
totalDeletions: number;
|
|
130
|
+
/** Unified patch text for the commit, or `null` when omitted/unavailable. */
|
|
131
|
+
patch: string | null;
|
|
132
|
+
/** `true` when `patch` was clipped to the internal char limit. */
|
|
133
|
+
truncated: boolean;
|
|
134
|
+
}
|
|
135
|
+
interface ShowArgs {
|
|
136
|
+
/** Commit-ish to inspect (full or short hash). */
|
|
137
|
+
hash: string;
|
|
138
|
+
/** Include the full unified patch (default `true`). */
|
|
139
|
+
patch?: boolean;
|
|
140
|
+
}
|
|
141
|
+
interface StageArgs {
|
|
142
|
+
/** Paths to stage (`git add`). */
|
|
143
|
+
paths: string[];
|
|
144
|
+
}
|
|
145
|
+
interface UnstageArgs {
|
|
146
|
+
/** Paths to unstage (`git restore --staged`). */
|
|
147
|
+
paths: string[];
|
|
148
|
+
}
|
|
149
|
+
interface CommitArgs {
|
|
150
|
+
/** Commit message. */
|
|
151
|
+
message: string;
|
|
152
|
+
}
|
|
153
|
+
interface CommitResult {
|
|
154
|
+
/** `true` when the commit succeeded. */
|
|
155
|
+
ok: boolean;
|
|
156
|
+
/** Short hash of the new commit, or `null` on failure. */
|
|
157
|
+
hash: string | null;
|
|
158
|
+
/** Human-readable outcome (e.g. "nothing to commit"). */
|
|
159
|
+
message: string;
|
|
160
|
+
/** Working-tree status after the attempt. */
|
|
161
|
+
status: GitStatus;
|
|
162
|
+
}
|
|
163
|
+
/** The node API a consumer gets from `ctx.services.get('@devframes/service-git')`. */
|
|
164
|
+
interface GitServiceApi {
|
|
165
|
+
status: () => Promise<GitStatus>;
|
|
166
|
+
log: (args?: LogArgs) => Promise<GitLog>;
|
|
167
|
+
show: (args: ShowArgs) => Promise<CommitDetail>;
|
|
168
|
+
diff: (args?: DiffArgs) => Promise<GitDiff>;
|
|
169
|
+
branches: () => Promise<GitBranches>;
|
|
170
|
+
stage: (args: StageArgs) => Promise<GitStatus>;
|
|
171
|
+
unstage: (args: UnstageArgs) => Promise<GitStatus>;
|
|
172
|
+
commit: (args: CommitArgs) => Promise<CommitResult>;
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/index.d.ts
|
|
176
|
+
declare const GIT_SERVICE_PACKAGE = "@devframes/service-git";
|
|
177
|
+
declare const GIT_SERVICE_SCOPE = "devframes:service:git";
|
|
178
|
+
interface GitServiceOptions {
|
|
179
|
+
/**
|
|
180
|
+
* Repository directory to operate on. Defaults to the context's `cwd`; the
|
|
181
|
+
* repo root is discovered once (`rev-parse --show-toplevel`) and memoized.
|
|
182
|
+
*/
|
|
183
|
+
cwd?: string;
|
|
184
|
+
}
|
|
185
|
+
declare module 'devframe' {
|
|
186
|
+
interface DevframeRpcServerFunctions {
|
|
187
|
+
'devframes:service:git:status': () => Promise<GitStatus>;
|
|
188
|
+
'devframes:service:git:log': (args?: LogArgs) => Promise<GitLog>;
|
|
189
|
+
'devframes:service:git:show': (args: ShowArgs) => Promise<CommitDetail>;
|
|
190
|
+
'devframes:service:git:diff': (args?: DiffArgs) => Promise<GitDiff>;
|
|
191
|
+
'devframes:service:git:branches': () => Promise<GitBranches>;
|
|
192
|
+
'devframes:service:git:stage': (args: StageArgs) => Promise<GitStatus>;
|
|
193
|
+
'devframes:service:git:unstage': (args: UnstageArgs) => Promise<GitStatus>;
|
|
194
|
+
'devframes:service:git:commit': (args: CommitArgs) => Promise<CommitResult>;
|
|
195
|
+
}
|
|
196
|
+
interface DevframeServicesRegistry {
|
|
197
|
+
'@devframes/service-git': GitServiceApi;
|
|
198
|
+
}
|
|
199
|
+
interface DevframeServicesScopeRegistry {
|
|
200
|
+
'@devframes/service-git': 'devframes:service:git';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The git wire service — read/write git operations shared over RPC by every
|
|
205
|
+
* plugin on the host, generalizing the utilities that used to live inside the
|
|
206
|
+
* git plugin. The exec wrapper and output parsers stay internal; consumers get
|
|
207
|
+
* the typed {@link GitServiceApi} in-process (`ctx.services.get`) and the same
|
|
208
|
+
* ops over `devframes:service:git:*` RPC. Write ops are always exposed —
|
|
209
|
+
* authorization is the host's connection-trust boundary. The service defines
|
|
210
|
+
* no `dump`/`snapshot`; a devframe bakes what it needs via `snapshotRpc`.
|
|
211
|
+
*/
|
|
212
|
+
declare function createGitService(options?: GitServiceOptions): DevframeServiceDefinition<GitServiceApi, GitServiceOptions>;
|
|
213
|
+
//#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 };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import { defineRpcFunction } from "devframe";
|
|
2
|
+
import { s } from "devframe/utils/simple-schema";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
//#region package.json
|
|
7
|
+
var version = "0.9.1";
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/git.ts
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const MAX_BUFFER = 67108864;
|
|
12
|
+
/**
|
|
13
|
+
* Run a git command in `cwd`. Rejects when git exits non-zero — callers that
|
|
14
|
+
* tolerate failure (e.g. "no upstream configured") should use {@link tryGit}.
|
|
15
|
+
*/
|
|
16
|
+
async function runGit(cwd, args) {
|
|
17
|
+
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
18
|
+
cwd,
|
|
19
|
+
maxBuffer: MAX_BUFFER,
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
GIT_PAGER: "cat",
|
|
24
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
25
|
+
LC_ALL: "C"
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
stdout,
|
|
30
|
+
stderr
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Run a git command, returning trimmed stdout or `null` when it fails. */
|
|
34
|
+
async function tryGit(cwd, args) {
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await runGit(cwd, args);
|
|
37
|
+
return stdout.replace(/\n$/, "");
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Caller-supplied revisions must not start with `-`, which Git would parse as
|
|
44
|
+
* an option before it treats the value as a revision.
|
|
45
|
+
*/
|
|
46
|
+
function isSafeRevision(rev) {
|
|
47
|
+
return rev.length > 0 && !rev.startsWith("-");
|
|
48
|
+
}
|
|
49
|
+
/** Resolve the repository root for `cwd`, or `null` when `cwd` is outside a repo. */
|
|
50
|
+
async function resolveRepoRoot(cwd) {
|
|
51
|
+
return tryGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
52
|
+
}
|
|
53
|
+
/** Split git output on a separator, dropping the trailing empty segment. */
|
|
54
|
+
function splitClean(input, separator) {
|
|
55
|
+
return input.split(separator).filter((part) => part.length > 0);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract a concise, single-line message from a failed `execFile` error.
|
|
59
|
+
* git writes useful text (e.g. "nothing to commit") to stdout/stderr.
|
|
60
|
+
*/
|
|
61
|
+
function gitErrorMessage(error) {
|
|
62
|
+
const e = error;
|
|
63
|
+
return (e?.stderr || e?.stdout || e?.message || "git command failed").trim().split("\n")[0];
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/operations.ts
|
|
67
|
+
/** Hard cap on returned patch text to keep payloads bounded. */
|
|
68
|
+
const PATCH_CHAR_LIMIT = 2e5;
|
|
69
|
+
const EMPTY_STATUS = {
|
|
70
|
+
isRepo: false,
|
|
71
|
+
root: null,
|
|
72
|
+
branch: null,
|
|
73
|
+
detached: false,
|
|
74
|
+
head: null,
|
|
75
|
+
upstream: null,
|
|
76
|
+
ahead: 0,
|
|
77
|
+
behind: 0,
|
|
78
|
+
staged: [],
|
|
79
|
+
unstaged: [],
|
|
80
|
+
untracked: [],
|
|
81
|
+
clean: true,
|
|
82
|
+
canWrite: false
|
|
83
|
+
};
|
|
84
|
+
function mapCode(code) {
|
|
85
|
+
switch (code) {
|
|
86
|
+
case "M": return "modified";
|
|
87
|
+
case "A": return "added";
|
|
88
|
+
case "D": return "deleted";
|
|
89
|
+
case "R": return "renamed";
|
|
90
|
+
case "C": return "copied";
|
|
91
|
+
case "T": return "type-changed";
|
|
92
|
+
case "U": return "unmerged";
|
|
93
|
+
default: return "unknown";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse `git status --porcelain=v2 --branch -z` into a structured snapshot.
|
|
98
|
+
* Records are NUL-separated; rename/copy (type `2`) entries consume an extra
|
|
99
|
+
* token for the original path.
|
|
100
|
+
*/
|
|
101
|
+
function parseStatus(root, raw) {
|
|
102
|
+
const tokens = raw.split("\0");
|
|
103
|
+
const status = {
|
|
104
|
+
...EMPTY_STATUS,
|
|
105
|
+
isRepo: true,
|
|
106
|
+
root,
|
|
107
|
+
staged: [],
|
|
108
|
+
unstaged: [],
|
|
109
|
+
untracked: []
|
|
110
|
+
};
|
|
111
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
112
|
+
const token = tokens[i];
|
|
113
|
+
if (!token) continue;
|
|
114
|
+
if (token.startsWith("# ")) {
|
|
115
|
+
const [, key, ...rest] = token.split(" ");
|
|
116
|
+
const value = rest.join(" ");
|
|
117
|
+
if (key === "branch.head") {
|
|
118
|
+
if (value === "(detached)") {
|
|
119
|
+
status.detached = true;
|
|
120
|
+
status.branch = null;
|
|
121
|
+
} else status.branch = value;
|
|
122
|
+
} else if (key === "branch.oid" && value !== "(initial)") status.head = value.slice(0, 9);
|
|
123
|
+
else if (key === "branch.upstream") status.upstream = value;
|
|
124
|
+
else if (key === "branch.ab") {
|
|
125
|
+
const match = value.match(/\+(\d+)\s+-(\d+)/);
|
|
126
|
+
if (match) {
|
|
127
|
+
status.ahead = Number(match[1]);
|
|
128
|
+
status.behind = Number(match[2]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (token.startsWith("1 ") || token.startsWith("2 ")) {
|
|
134
|
+
const renamed = token.startsWith("2 ");
|
|
135
|
+
const fields = token.split(" ");
|
|
136
|
+
const xy = fields[1];
|
|
137
|
+
const x = xy[0];
|
|
138
|
+
const y = xy[1];
|
|
139
|
+
const path = fields.slice(renamed ? 9 : 8).join(" ");
|
|
140
|
+
const from = renamed ? tokens[++i] : void 0;
|
|
141
|
+
if (x !== ".") status.staged.push(from ? {
|
|
142
|
+
path,
|
|
143
|
+
from,
|
|
144
|
+
status: mapCode(x)
|
|
145
|
+
} : {
|
|
146
|
+
path,
|
|
147
|
+
status: mapCode(x)
|
|
148
|
+
});
|
|
149
|
+
if (y !== ".") status.unstaged.push({
|
|
150
|
+
path,
|
|
151
|
+
status: mapCode(y)
|
|
152
|
+
});
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (token.startsWith("u ")) {
|
|
156
|
+
const path = token.split(" ").slice(10).join(" ");
|
|
157
|
+
status.unstaged.push({
|
|
158
|
+
path,
|
|
159
|
+
status: "unmerged"
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (token.startsWith("? ")) status.untracked.push(token.slice(2));
|
|
164
|
+
}
|
|
165
|
+
status.clean = status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0;
|
|
166
|
+
return status;
|
|
167
|
+
}
|
|
168
|
+
const LOG_FORMAT = [
|
|
169
|
+
"%H",
|
|
170
|
+
"%h",
|
|
171
|
+
"%P",
|
|
172
|
+
"%an",
|
|
173
|
+
"%ae",
|
|
174
|
+
"%aI",
|
|
175
|
+
"%D",
|
|
176
|
+
"%s",
|
|
177
|
+
"%b"
|
|
178
|
+
].join("") + "";
|
|
179
|
+
function clamp(value, min, max) {
|
|
180
|
+
return Math.min(Math.max(value, min), max);
|
|
181
|
+
}
|
|
182
|
+
function parseLog(raw) {
|
|
183
|
+
return splitClean(raw, "").map((record) => {
|
|
184
|
+
const [hash, shortHash, parents, author, email, isoDate, refs, subject, body] = record.replace(/^\n/, "").split("");
|
|
185
|
+
return {
|
|
186
|
+
hash,
|
|
187
|
+
shortHash,
|
|
188
|
+
author,
|
|
189
|
+
email,
|
|
190
|
+
date: Date.parse(isoDate),
|
|
191
|
+
subject,
|
|
192
|
+
body: (body ?? "").trim(),
|
|
193
|
+
refs: refs ? refs.split(", ").map((r) => r.trim()).filter(Boolean) : [],
|
|
194
|
+
parents: parents ? parents.split(" ").filter(Boolean) : []
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const BRANCH_FORMAT = [
|
|
199
|
+
"%(refname:short)",
|
|
200
|
+
"%(objectname:short)",
|
|
201
|
+
"%(HEAD)",
|
|
202
|
+
"%(upstream:short)",
|
|
203
|
+
"%(upstream:track)",
|
|
204
|
+
"%(contents:subject)"
|
|
205
|
+
].join("");
|
|
206
|
+
function parseTrack(track) {
|
|
207
|
+
if (track.includes("gone")) return {
|
|
208
|
+
ahead: 0,
|
|
209
|
+
behind: 0,
|
|
210
|
+
gone: true
|
|
211
|
+
};
|
|
212
|
+
const ahead = track.match(/ahead (\d+)/);
|
|
213
|
+
const behind = track.match(/behind (\d+)/);
|
|
214
|
+
return {
|
|
215
|
+
ahead: ahead ? Number(ahead[1]) : 0,
|
|
216
|
+
behind: behind ? Number(behind[1]) : 0,
|
|
217
|
+
gone: false
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function parseNumstat(raw) {
|
|
221
|
+
return splitClean(raw, "\n").map((line) => {
|
|
222
|
+
const [add, del, ...rest] = line.split(" ");
|
|
223
|
+
const binary = add === "-" || del === "-";
|
|
224
|
+
return {
|
|
225
|
+
path: rest.join(" "),
|
|
226
|
+
additions: binary ? 0 : Number(add),
|
|
227
|
+
deletions: binary ? 0 : Number(del),
|
|
228
|
+
binary
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const SHOW_FORMAT = [
|
|
233
|
+
"%H",
|
|
234
|
+
"%h",
|
|
235
|
+
"%P",
|
|
236
|
+
"%an",
|
|
237
|
+
"%ae",
|
|
238
|
+
"%aI",
|
|
239
|
+
"%cn",
|
|
240
|
+
"%ce",
|
|
241
|
+
"%cI",
|
|
242
|
+
"%D",
|
|
243
|
+
"%s",
|
|
244
|
+
"%b"
|
|
245
|
+
].join("");
|
|
246
|
+
const EMPTY_DETAIL = {
|
|
247
|
+
isRepo: false,
|
|
248
|
+
found: false,
|
|
249
|
+
hash: "",
|
|
250
|
+
shortHash: "",
|
|
251
|
+
author: "",
|
|
252
|
+
email: "",
|
|
253
|
+
date: 0,
|
|
254
|
+
committer: "",
|
|
255
|
+
committerEmail: "",
|
|
256
|
+
commitDate: 0,
|
|
257
|
+
subject: "",
|
|
258
|
+
body: "",
|
|
259
|
+
parents: [],
|
|
260
|
+
refs: [],
|
|
261
|
+
files: [],
|
|
262
|
+
totalAdditions: 0,
|
|
263
|
+
totalDeletions: 0,
|
|
264
|
+
patch: null,
|
|
265
|
+
truncated: false
|
|
266
|
+
};
|
|
267
|
+
function mapStatusCode(code) {
|
|
268
|
+
switch (code[0]) {
|
|
269
|
+
case "M": return "modified";
|
|
270
|
+
case "A": return "added";
|
|
271
|
+
case "D": return "deleted";
|
|
272
|
+
case "R": return "renamed";
|
|
273
|
+
case "C": return "copied";
|
|
274
|
+
case "T": return "type-changed";
|
|
275
|
+
case "U": return "unmerged";
|
|
276
|
+
default: return "unknown";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function parseNameStatus(raw) {
|
|
280
|
+
const map = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const line of splitClean(raw, "\n")) {
|
|
282
|
+
const [code, ...paths] = line.split(" ");
|
|
283
|
+
const path = paths[paths.length - 1];
|
|
284
|
+
if (path) map.set(path, mapStatusCode(code));
|
|
285
|
+
}
|
|
286
|
+
return map;
|
|
287
|
+
}
|
|
288
|
+
function parseCommitNumstat(raw, status) {
|
|
289
|
+
return splitClean(raw, "\n").map((line) => {
|
|
290
|
+
const [add, del, ...rest] = line.split(" ");
|
|
291
|
+
const binary = add === "-" || del === "-";
|
|
292
|
+
const path = rest.join(" ");
|
|
293
|
+
return {
|
|
294
|
+
path,
|
|
295
|
+
additions: binary ? 0 : Number(add),
|
|
296
|
+
deletions: binary ? 0 : Number(del),
|
|
297
|
+
binary,
|
|
298
|
+
status: status.get(path) ?? "modified"
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function clipPatch(raw) {
|
|
303
|
+
return raw.length > PATCH_CHAR_LIMIT ? {
|
|
304
|
+
patch: raw.slice(0, PATCH_CHAR_LIMIT),
|
|
305
|
+
truncated: true
|
|
306
|
+
} : {
|
|
307
|
+
patch: raw,
|
|
308
|
+
truncated: false
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Build the git node API bound to a single working directory, with the repo
|
|
313
|
+
* root resolved once (memoized). This is the surface returned to in-process
|
|
314
|
+
* consumers and wrapped by the service's RPC.
|
|
315
|
+
*/
|
|
316
|
+
function createGitOps(cwd) {
|
|
317
|
+
let rootPromise;
|
|
318
|
+
const resolveRoot = () => rootPromise ??= resolveRepoRoot(cwd);
|
|
319
|
+
async function status() {
|
|
320
|
+
const root = await resolveRoot();
|
|
321
|
+
if (!root) return { ...EMPTY_STATUS };
|
|
322
|
+
const { stdout } = await runGit(cwd, [
|
|
323
|
+
"status",
|
|
324
|
+
"--porcelain=v2",
|
|
325
|
+
"--branch",
|
|
326
|
+
"-z"
|
|
327
|
+
]);
|
|
328
|
+
const result = parseStatus(root, stdout);
|
|
329
|
+
result.canWrite = true;
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
async function readCommit(hash, includePatch) {
|
|
333
|
+
if (!isSafeRevision(hash)) return {
|
|
334
|
+
...EMPTY_DETAIL,
|
|
335
|
+
isRepo: true
|
|
336
|
+
};
|
|
337
|
+
const meta = await tryGit(cwd, [
|
|
338
|
+
"show",
|
|
339
|
+
"-s",
|
|
340
|
+
`--format=${SHOW_FORMAT}`,
|
|
341
|
+
"--end-of-options",
|
|
342
|
+
hash
|
|
343
|
+
]);
|
|
344
|
+
if (meta == null) return {
|
|
345
|
+
...EMPTY_DETAIL,
|
|
346
|
+
isRepo: true
|
|
347
|
+
};
|
|
348
|
+
const [fullHash, shortHash, parents, author, email, authorDate, committer, committerEmail, committerDate, refs, subject, body] = meta.split("");
|
|
349
|
+
const numstat = await tryGit(cwd, [
|
|
350
|
+
"diff-tree",
|
|
351
|
+
"--no-commit-id",
|
|
352
|
+
"--numstat",
|
|
353
|
+
"-r",
|
|
354
|
+
"--root",
|
|
355
|
+
"--end-of-options",
|
|
356
|
+
hash
|
|
357
|
+
]);
|
|
358
|
+
const nameStatusRaw = await tryGit(cwd, [
|
|
359
|
+
"diff-tree",
|
|
360
|
+
"--no-commit-id",
|
|
361
|
+
"--name-status",
|
|
362
|
+
"-r",
|
|
363
|
+
"--root",
|
|
364
|
+
"--end-of-options",
|
|
365
|
+
hash
|
|
366
|
+
]);
|
|
367
|
+
const files = numstat ? parseCommitNumstat(numstat, nameStatusRaw ? parseNameStatus(nameStatusRaw) : /* @__PURE__ */ new Map()) : [];
|
|
368
|
+
let patch = null;
|
|
369
|
+
let truncated = false;
|
|
370
|
+
if (includePatch) {
|
|
371
|
+
const raw = await tryGit(cwd, [
|
|
372
|
+
"diff-tree",
|
|
373
|
+
"-p",
|
|
374
|
+
"--no-commit-id",
|
|
375
|
+
"-r",
|
|
376
|
+
"--root",
|
|
377
|
+
"--end-of-options",
|
|
378
|
+
hash
|
|
379
|
+
]);
|
|
380
|
+
if (raw != null) ({patch, truncated} = clipPatch(raw));
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
isRepo: true,
|
|
384
|
+
found: true,
|
|
385
|
+
hash: fullHash,
|
|
386
|
+
shortHash,
|
|
387
|
+
author,
|
|
388
|
+
email,
|
|
389
|
+
date: Date.parse(authorDate),
|
|
390
|
+
committer,
|
|
391
|
+
committerEmail,
|
|
392
|
+
commitDate: Date.parse(committerDate),
|
|
393
|
+
subject,
|
|
394
|
+
body: (body ?? "").trim(),
|
|
395
|
+
parents: parents ? parents.split(" ").filter(Boolean) : [],
|
|
396
|
+
refs: refs ? refs.split(", ").map((r) => r.trim()).filter(Boolean) : [],
|
|
397
|
+
files,
|
|
398
|
+
totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
|
|
399
|
+
totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
|
|
400
|
+
patch,
|
|
401
|
+
truncated
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
status,
|
|
406
|
+
async log(args = {}) {
|
|
407
|
+
const limit = clamp(Math.trunc(args.limit ?? 30), 1, 200);
|
|
408
|
+
const skip = Math.max(0, Math.trunc(args.skip ?? 0));
|
|
409
|
+
const ref = args.ref?.trim() || void 0;
|
|
410
|
+
if (!await resolveRoot()) return {
|
|
411
|
+
isRepo: false,
|
|
412
|
+
commits: [],
|
|
413
|
+
limit,
|
|
414
|
+
skip,
|
|
415
|
+
hasMore: false
|
|
416
|
+
};
|
|
417
|
+
const command = [
|
|
418
|
+
"log",
|
|
419
|
+
"--topo-order",
|
|
420
|
+
`--max-count=${limit}`,
|
|
421
|
+
`--skip=${skip}`,
|
|
422
|
+
`--pretty=format:${LOG_FORMAT}`
|
|
423
|
+
];
|
|
424
|
+
if (ref) {
|
|
425
|
+
if (!isSafeRevision(ref)) return {
|
|
426
|
+
isRepo: true,
|
|
427
|
+
commits: [],
|
|
428
|
+
limit,
|
|
429
|
+
skip,
|
|
430
|
+
hasMore: false
|
|
431
|
+
};
|
|
432
|
+
command.push("--end-of-options", ref);
|
|
433
|
+
}
|
|
434
|
+
const raw = await tryGit(cwd, command);
|
|
435
|
+
const commits = raw ? parseLog(raw) : [];
|
|
436
|
+
return {
|
|
437
|
+
isRepo: true,
|
|
438
|
+
commits,
|
|
439
|
+
limit,
|
|
440
|
+
skip,
|
|
441
|
+
hasMore: commits.length === limit
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
async show(args) {
|
|
445
|
+
const hash = (args?.hash ?? "").trim();
|
|
446
|
+
const includePatch = args?.patch ?? true;
|
|
447
|
+
if (!await resolveRoot() || !hash) return { ...EMPTY_DETAIL };
|
|
448
|
+
return readCommit(hash, includePatch);
|
|
449
|
+
},
|
|
450
|
+
async diff(args = {}) {
|
|
451
|
+
const { path, staged = false } = args;
|
|
452
|
+
if (!await resolveRoot()) return {
|
|
453
|
+
isRepo: false,
|
|
454
|
+
staged,
|
|
455
|
+
path: path ?? null,
|
|
456
|
+
files: [],
|
|
457
|
+
totalAdditions: 0,
|
|
458
|
+
totalDeletions: 0,
|
|
459
|
+
patch: null,
|
|
460
|
+
truncated: false
|
|
461
|
+
};
|
|
462
|
+
const base = staged ? ["diff", "--cached"] : ["diff"];
|
|
463
|
+
const scope = path ? ["--", path] : [];
|
|
464
|
+
const numstatRaw = await tryGit(cwd, [
|
|
465
|
+
...base,
|
|
466
|
+
"--numstat",
|
|
467
|
+
...scope
|
|
468
|
+
]);
|
|
469
|
+
const files = numstatRaw ? parseNumstat(numstatRaw) : [];
|
|
470
|
+
let patch = null;
|
|
471
|
+
let truncated = false;
|
|
472
|
+
if (path) {
|
|
473
|
+
const { stdout } = await runGit(cwd, [...base, ...scope]);
|
|
474
|
+
({patch, truncated} = clipPatch(stdout));
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
isRepo: true,
|
|
478
|
+
staged,
|
|
479
|
+
path: path ?? null,
|
|
480
|
+
files,
|
|
481
|
+
totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
|
|
482
|
+
totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
|
|
483
|
+
patch,
|
|
484
|
+
truncated
|
|
485
|
+
};
|
|
486
|
+
},
|
|
487
|
+
async branches() {
|
|
488
|
+
if (!await resolveRoot()) return {
|
|
489
|
+
isRepo: false,
|
|
490
|
+
current: null,
|
|
491
|
+
branches: []
|
|
492
|
+
};
|
|
493
|
+
const raw = await tryGit(cwd, [
|
|
494
|
+
"for-each-ref",
|
|
495
|
+
`--format=${BRANCH_FORMAT}`,
|
|
496
|
+
"refs/heads"
|
|
497
|
+
]);
|
|
498
|
+
if (!raw) return {
|
|
499
|
+
isRepo: true,
|
|
500
|
+
current: null,
|
|
501
|
+
branches: []
|
|
502
|
+
};
|
|
503
|
+
let current = null;
|
|
504
|
+
const branches = splitClean(raw, "\n").map((line) => {
|
|
505
|
+
const [name, sha, head, upstream, track, subject] = line.split("");
|
|
506
|
+
const isCurrent = head === "*";
|
|
507
|
+
if (isCurrent) current = name;
|
|
508
|
+
return {
|
|
509
|
+
name,
|
|
510
|
+
current: isCurrent,
|
|
511
|
+
sha,
|
|
512
|
+
upstream: upstream || null,
|
|
513
|
+
subject: subject ?? "",
|
|
514
|
+
...parseTrack(track ?? "")
|
|
515
|
+
};
|
|
516
|
+
});
|
|
517
|
+
branches.sort((a, b) => Number(b.current) - Number(a.current));
|
|
518
|
+
return {
|
|
519
|
+
isRepo: true,
|
|
520
|
+
current,
|
|
521
|
+
branches
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
async stage(args) {
|
|
525
|
+
const paths = args?.paths ?? [];
|
|
526
|
+
if (await resolveRoot() && paths.length > 0) await runGit(cwd, [
|
|
527
|
+
"add",
|
|
528
|
+
"--",
|
|
529
|
+
...paths
|
|
530
|
+
]);
|
|
531
|
+
return status();
|
|
532
|
+
},
|
|
533
|
+
async unstage(args) {
|
|
534
|
+
const paths = args?.paths ?? [];
|
|
535
|
+
if (await resolveRoot() && paths.length > 0) await runGit(cwd, [
|
|
536
|
+
"restore",
|
|
537
|
+
"--staged",
|
|
538
|
+
"--",
|
|
539
|
+
...paths
|
|
540
|
+
]);
|
|
541
|
+
return status();
|
|
542
|
+
},
|
|
543
|
+
async commit(args) {
|
|
544
|
+
const message = (args?.message ?? "").trim();
|
|
545
|
+
if (!await resolveRoot()) return {
|
|
546
|
+
ok: false,
|
|
547
|
+
hash: null,
|
|
548
|
+
message: "Not a git repository.",
|
|
549
|
+
status: await status()
|
|
550
|
+
};
|
|
551
|
+
if (!message) return {
|
|
552
|
+
ok: false,
|
|
553
|
+
hash: null,
|
|
554
|
+
message: "Commit message is required.",
|
|
555
|
+
status: await status()
|
|
556
|
+
};
|
|
557
|
+
try {
|
|
558
|
+
await runGit(cwd, [
|
|
559
|
+
"commit",
|
|
560
|
+
"-m",
|
|
561
|
+
message
|
|
562
|
+
]);
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
hash: await tryGit(cwd, [
|
|
566
|
+
"rev-parse",
|
|
567
|
+
"--short",
|
|
568
|
+
"HEAD"
|
|
569
|
+
]),
|
|
570
|
+
message: "Committed.",
|
|
571
|
+
status: await status()
|
|
572
|
+
};
|
|
573
|
+
} catch (error) {
|
|
574
|
+
return {
|
|
575
|
+
ok: false,
|
|
576
|
+
hash: null,
|
|
577
|
+
message: gitErrorMessage(error),
|
|
578
|
+
status: await status()
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/index.ts
|
|
586
|
+
const GIT_SERVICE_PACKAGE = "@devframes/service-git";
|
|
587
|
+
const GIT_SERVICE_SCOPE = "devframes:service:git";
|
|
588
|
+
const commitSchema = s.object({
|
|
589
|
+
hash: s.string(),
|
|
590
|
+
shortHash: s.string(),
|
|
591
|
+
author: s.string(),
|
|
592
|
+
email: s.string(),
|
|
593
|
+
date: s.number(),
|
|
594
|
+
subject: s.string(),
|
|
595
|
+
body: s.string(),
|
|
596
|
+
refs: s.array(s.string()),
|
|
597
|
+
parents: s.array(s.string())
|
|
598
|
+
});
|
|
599
|
+
const gitLogSchema = s.object({
|
|
600
|
+
isRepo: s.boolean(),
|
|
601
|
+
commits: s.array(commitSchema),
|
|
602
|
+
limit: s.number(),
|
|
603
|
+
skip: s.number(),
|
|
604
|
+
hasMore: s.boolean()
|
|
605
|
+
});
|
|
606
|
+
const fileStatusCodeSchema = s.picklist([
|
|
607
|
+
"modified",
|
|
608
|
+
"added",
|
|
609
|
+
"deleted",
|
|
610
|
+
"renamed",
|
|
611
|
+
"copied",
|
|
612
|
+
"type-changed",
|
|
613
|
+
"unmerged",
|
|
614
|
+
"unknown"
|
|
615
|
+
]);
|
|
616
|
+
const commitDetailSchema = s.object({
|
|
617
|
+
isRepo: s.boolean(),
|
|
618
|
+
found: s.boolean(),
|
|
619
|
+
hash: s.string(),
|
|
620
|
+
shortHash: s.string(),
|
|
621
|
+
author: s.string(),
|
|
622
|
+
email: s.string(),
|
|
623
|
+
date: s.number(),
|
|
624
|
+
committer: s.string(),
|
|
625
|
+
committerEmail: s.string(),
|
|
626
|
+
commitDate: s.number(),
|
|
627
|
+
subject: s.string(),
|
|
628
|
+
body: s.string(),
|
|
629
|
+
parents: s.array(s.string()),
|
|
630
|
+
refs: s.array(s.string()),
|
|
631
|
+
files: s.array(s.object({
|
|
632
|
+
path: s.string(),
|
|
633
|
+
additions: s.number(),
|
|
634
|
+
deletions: s.number(),
|
|
635
|
+
binary: s.boolean(),
|
|
636
|
+
status: fileStatusCodeSchema
|
|
637
|
+
})),
|
|
638
|
+
totalAdditions: s.number(),
|
|
639
|
+
totalDeletions: s.number(),
|
|
640
|
+
patch: s.nullable(s.string()),
|
|
641
|
+
truncated: s.boolean()
|
|
642
|
+
});
|
|
643
|
+
const gitDiffSchema = s.object({
|
|
644
|
+
isRepo: s.boolean(),
|
|
645
|
+
staged: s.boolean(),
|
|
646
|
+
path: s.nullable(s.string()),
|
|
647
|
+
files: s.array(s.object({
|
|
648
|
+
path: s.string(),
|
|
649
|
+
additions: s.number(),
|
|
650
|
+
deletions: s.number(),
|
|
651
|
+
binary: s.boolean()
|
|
652
|
+
})),
|
|
653
|
+
totalAdditions: s.number(),
|
|
654
|
+
totalDeletions: s.number(),
|
|
655
|
+
patch: s.nullable(s.string()),
|
|
656
|
+
truncated: s.boolean()
|
|
657
|
+
});
|
|
658
|
+
/**
|
|
659
|
+
* The git wire service — read/write git operations shared over RPC by every
|
|
660
|
+
* plugin on the host, generalizing the utilities that used to live inside the
|
|
661
|
+
* git plugin. The exec wrapper and output parsers stay internal; consumers get
|
|
662
|
+
* the typed {@link GitServiceApi} in-process (`ctx.services.get`) and the same
|
|
663
|
+
* ops over `devframes:service:git:*` RPC. Write ops are always exposed —
|
|
664
|
+
* authorization is the host's connection-trust boundary. The service defines
|
|
665
|
+
* no `dump`/`snapshot`; a devframe bakes what it needs via `snapshotRpc`.
|
|
666
|
+
*/
|
|
667
|
+
function createGitService(options) {
|
|
668
|
+
return {
|
|
669
|
+
package: GIT_SERVICE_PACKAGE,
|
|
670
|
+
version,
|
|
671
|
+
scope: GIT_SERVICE_SCOPE,
|
|
672
|
+
options,
|
|
673
|
+
setup(ctx, { options }) {
|
|
674
|
+
const ops = createGitOps(options?.cwd ?? ctx.cwd);
|
|
675
|
+
ctx.rpc.register(defineRpcFunction({
|
|
676
|
+
name: "status",
|
|
677
|
+
type: "query",
|
|
678
|
+
jsonSerializable: true,
|
|
679
|
+
agent: {
|
|
680
|
+
title: "Git status",
|
|
681
|
+
description: "Working-tree status of the inspected repository: current branch, ahead/behind counts, and every staged/unstaged/untracked file. Safe to call freely."
|
|
682
|
+
},
|
|
683
|
+
handler: () => ops.status()
|
|
684
|
+
}));
|
|
685
|
+
ctx.rpc.register(defineRpcFunction({
|
|
686
|
+
name: "log",
|
|
687
|
+
type: "query",
|
|
688
|
+
jsonSerializable: true,
|
|
689
|
+
args: [s.object({
|
|
690
|
+
limit: s.optional(s.number()),
|
|
691
|
+
skip: s.optional(s.number()),
|
|
692
|
+
ref: s.optional(s.string())
|
|
693
|
+
})],
|
|
694
|
+
returns: gitLogSchema,
|
|
695
|
+
agent: {
|
|
696
|
+
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."
|
|
698
|
+
},
|
|
699
|
+
handler: (args = {}) => ops.log(args)
|
|
700
|
+
}));
|
|
701
|
+
ctx.rpc.register(defineRpcFunction({
|
|
702
|
+
name: "show",
|
|
703
|
+
type: "query",
|
|
704
|
+
jsonSerializable: true,
|
|
705
|
+
args: [s.object({
|
|
706
|
+
hash: s.string(),
|
|
707
|
+
patch: s.optional(s.boolean())
|
|
708
|
+
})],
|
|
709
|
+
returns: commitDetailSchema,
|
|
710
|
+
agent: {
|
|
711
|
+
title: "Git show",
|
|
712
|
+
description: "Full detail of one commit by hash: metadata, changed files, and the unified patch (pass patch: false to skip it for large commits). Safe to call freely."
|
|
713
|
+
},
|
|
714
|
+
handler: (args) => ops.show(args)
|
|
715
|
+
}));
|
|
716
|
+
ctx.rpc.register(defineRpcFunction({
|
|
717
|
+
name: "diff",
|
|
718
|
+
type: "query",
|
|
719
|
+
jsonSerializable: true,
|
|
720
|
+
args: [s.object({
|
|
721
|
+
path: s.optional(s.string()),
|
|
722
|
+
staged: s.optional(s.boolean())
|
|
723
|
+
})],
|
|
724
|
+
returns: gitDiffSchema,
|
|
725
|
+
agent: {
|
|
726
|
+
title: "Git diff",
|
|
727
|
+
description: "Unified diff of uncommitted changes — the working tree by default, the index with staged: true, one file with path. Safe to call freely."
|
|
728
|
+
},
|
|
729
|
+
handler: (args = {}) => ops.diff(args)
|
|
730
|
+
}));
|
|
731
|
+
ctx.rpc.register(defineRpcFunction({
|
|
732
|
+
name: "branches",
|
|
733
|
+
type: "query",
|
|
734
|
+
jsonSerializable: true,
|
|
735
|
+
agent: {
|
|
736
|
+
title: "Git branches",
|
|
737
|
+
description: "List local branches with tracking state (ahead/behind, gone upstreams) and the current branch. Safe to call freely."
|
|
738
|
+
},
|
|
739
|
+
handler: () => ops.branches()
|
|
740
|
+
}));
|
|
741
|
+
ctx.rpc.register(defineRpcFunction({
|
|
742
|
+
name: "stage",
|
|
743
|
+
type: "action",
|
|
744
|
+
jsonSerializable: true,
|
|
745
|
+
handler: (args) => ops.stage(args)
|
|
746
|
+
}));
|
|
747
|
+
ctx.rpc.register(defineRpcFunction({
|
|
748
|
+
name: "unstage",
|
|
749
|
+
type: "action",
|
|
750
|
+
jsonSerializable: true,
|
|
751
|
+
handler: (args) => ops.unstage(args)
|
|
752
|
+
}));
|
|
753
|
+
ctx.rpc.register(defineRpcFunction({
|
|
754
|
+
name: "commit",
|
|
755
|
+
type: "action",
|
|
756
|
+
jsonSerializable: true,
|
|
757
|
+
handler: (args) => ops.commit(args)
|
|
758
|
+
}));
|
|
759
|
+
return ops;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
export { GIT_SERVICE_PACKAGE, GIT_SERVICE_SCOPE, createGitService, createGitService as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devframes/service-git",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.9.1",
|
|
5
|
+
"description": "Devframe wire service exposing read/write git operations (status, log, show, diff, branches, stage, unstage, commit) over RPC.",
|
|
6
|
+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/devframes/devframe#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"directory": "services/git",
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/devframes/devframe.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": "https://github.com/devframes/devframe/issues",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"devframe",
|
|
17
|
+
"devframe-service",
|
|
18
|
+
"devtools",
|
|
19
|
+
"git"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.mjs",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"devframe": "0.9.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^26.2.0",
|
|
35
|
+
"tsdown": "^0.22.14",
|
|
36
|
+
"vitest": "^4.1.10",
|
|
37
|
+
"devframe": "0.9.2"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"watch": "tsdown --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|