Skip to main content

submilli_engine/stdlib/git/
declaration.rs

1//! The `submilli:git` package declaration: repository constructors and methods.
2
3use std::collections::BTreeMap;
4
5use crate::{
6    DefaultValue, FieldSig, FileId, MethodSig, ObjectField, PackageDeclaration, Param, Span, Type,
7    TypeKind, TypeSymbol, Visibility,
8};
9
10use super::MODULE_NAME;
11
12pub fn package_declaration() -> PackageDeclaration {
13    let mut defs = PackageDeclaration::with_package(MODULE_NAME);
14    let mut statics = BTreeMap::new();
15    insert_static(
16        &mut statics,
17        "open",
18        vec![string("path")],
19        repository_type(),
20        "/** Open an ordinary repository under the VFS root. Gated operations check the current caller's grants. */",
21    );
22    insert_static(
23        &mut statics,
24        "init",
25        vec![string("path"), options(&[("branch", Type::String)])],
26        repository_type(),
27        "/** Initialize a repository, default branch main. Requires blueprint Git identity.\n * @capability git.init { path }\n */",
28    );
29    insert_static(
30        &mut statics,
31        "clone",
32        vec![
33            string("url"),
34            string("path"),
35            options(&[("branch", Type::String)]),
36        ],
37        repository_type(),
38        "/** Clone HTTPS into an empty VFS directory, following remote HEAD unless branch is specified. Requires only git.clone; authentication uses host-only GIT_TOKEN when needed.\n * @capability git.clone { path, remote: $url, remoteName: \"origin\", branch: string }\n */",
39    );
40    insert_repository_class(&mut defs, statics);
41    defs
42}
43
44fn insert_static(
45    statics: &mut BTreeMap<String, MethodSig>,
46    name: &str,
47    params: Vec<Param>,
48    ret: Type,
49    doc: &str,
50) {
51    statics.insert(
52        name.into(),
53        MethodSig {
54            optional: false,
55            generics: vec![],
56            params,
57            ret,
58            predicate: None,
59            doc: crate::doc(FileId::GIT, doc),
60        },
61    );
62}
63
64fn insert_repository_class(defs: &mut PackageDeclaration, statics: BTreeMap<String, MethodSig>) {
65    let mut methods = BTreeMap::new();
66    let entry = object(
67        &[
68            ("path", Type::String),
69            ("staged", Type::String),
70            ("unstaged", Type::String),
71            ("untracked", Type::Boolean),
72        ],
73        false,
74    );
75    let commit = object(
76        &[
77            ("id", Type::String),
78            ("message", Type::String),
79            ("authorName", Type::String),
80            ("authorEmail", Type::String),
81        ],
82        false,
83    );
84    methods.insert(
85        "status".into(),
86        MethodSig {
87            optional: false,
88            generics: vec![],
89            params: vec![],
90            ret: with_optional(
91                object(
92                    &[("entries", array(entry)), ("clean", Type::Boolean)],
93                    false,
94                ),
95                "branch",
96                Type::String,
97            ),
98            predicate: None,
99            doc: crate::doc(
100                FileId::GIT,
101                "/** Inspect staged, unstaged and untracked paths. */",
102            ),
103        },
104    );
105    methods.insert(
106        "log".into(),
107        MethodSig {
108            optional: false,
109            generics: vec![],
110            params: vec![options(&[
111                ("limit", Type::Number),
112                ("offset", Type::Number),
113            ])],
114            ret: with_optional(
115                object(&[("commits", array(commit))], false),
116                "nextOffset",
117                Type::Number,
118            ),
119            predicate: None,
120            doc: crate::doc(FileId::GIT, "/** Read history, default 50 commits; limit 1..1000, nonnegative integer offset. Traversal is resource-bounded. */"),
121        },
122    );
123    methods.insert(
124        "diff".into(),
125        MethodSig {
126            optional: false,
127            generics: vec![],
128            params: vec![options(&[
129                ("mode", Type::String),
130                ("from", Type::String),
131                ("to", Type::String),
132            ])],
133            ret: object(
134                &[
135                    ("patch", Type::String),
136                    ("binaryPaths", array(Type::String)),
137                ],
138                false,
139            ),
140            predicate: None,
141            doc: crate::doc(FileId::GIT, "/** Compare working (default), staged, or refs (requires from and to). Bounded patches and binary path markers. */"),
142        },
143    );
144    methods.insert(
145        "show".into(),
146        MethodSig {
147            optional: false,
148            generics: vec![],
149            params: vec![string("ref"), string("path")],
150            ret: Type::Uint8Array,
151            predicate: None,
152            doc: crate::doc(FileId::GIT, "/** Read file bytes from a commit. */"),
153        },
154    );
155    methods.insert(
156        "branches".into(),
157        MethodSig {
158            optional: false,
159            generics: vec![],
160            params: vec![],
161            ret: array(object(
162                &[
163                    ("name", Type::String),
164                    ("id", Type::String),
165                    ("current", Type::Boolean),
166                ],
167                false,
168            )),
169            predicate: None,
170            doc: crate::doc(FileId::GIT, "/** List local branches. */"),
171        },
172    );
173    methods.insert(
174        "remotes".into(),
175        MethodSig {
176            optional: false,
177            generics: vec![],
178            params: vec![],
179            ret: array(object(
180                &[("name", Type::String), ("url", Type::String)],
181                false,
182            )),
183            predicate: None,
184            doc: crate::doc(
185                FileId::GIT,
186                "/** List named remotes without credentials. */",
187            ),
188        },
189    );
190    methods.insert(
191        "add".into(),
192        MethodSig {
193            optional: false,
194            generics: vec![],
195            params: vec![Param::new("paths", array(Type::String))],
196            ret: Type::Void,
197            predicate: None,
198            doc: crate::doc(FileId::GIT, "/** Stage explicit files or directories, including deletions. Use . for the working tree. */"),
199        },
200    );
201    methods.insert(
202        "commit".into(),
203        MethodSig {
204            optional: false,
205            generics: vec![],
206            params: vec![string("message")],
207            ret: Type::String,
208            predicate: None,
209            doc: crate::doc(FileId::GIT, "/** Commit staged changes with Blueprint identity and return the commit ID. Empty commits are refused.\n * @capability git.commit { path: string, branch: string }\n */"),
210        },
211    );
212    methods.insert(
213        "createBranch".into(),
214        MethodSig {
215            optional: false,
216            generics: vec![],
217            params: vec![string("name"), default_string("start", "HEAD")],
218            ret: Type::Void,
219            predicate: None,
220            doc: crate::doc(
221                FileId::GIT,
222                "/** Create a branch without overwriting an existing branch. */",
223            ),
224        },
225    );
226    methods.insert(
227        "switchBranch".into(),
228        MethodSig {
229            optional: false,
230            generics: vec![],
231            params: vec![string("name")],
232            ret: Type::Void,
233            predicate: None,
234            doc: crate::doc(
235                FileId::GIT,
236                "/** Switch local branches; requires a completely clean working tree. */",
237            ),
238        },
239    );
240    methods.insert(
241        "addRemote".into(),
242        MethodSig {
243            optional: false,
244            generics: vec![],
245            params: vec![string("name"), string("url")],
246            ret: Type::Void,
247            predicate: None,
248            doc: crate::doc(
249                FileId::GIT,
250                "/** Add a named HTTPS remote, for example origin or upstream. */",
251            ),
252        },
253    );
254    methods.insert(
255        "setRemoteUrl".into(),
256        MethodSig {
257            optional: false,
258            generics: vec![],
259            params: vec![string("name"), string("url")],
260            ret: Type::Void,
261            predicate: None,
262            doc: crate::doc(FileId::GIT, "/** Update an existing remote's HTTPS URL. */"),
263        },
264    );
265    methods.insert(
266        "fetch".into(),
267        MethodSig {
268            optional: false,
269            generics: vec![],
270            params: vec![
271                default_string("remote", "origin"),
272                default_string("branch", ""),
273            ],
274            ret: object(&[("branches", array(Type::String))], false),
275            predicate: None,
276            doc: crate::doc(FileId::GIT, "/** Fetch remote-tracking branches. Empty branch requests every remote branch; every selected branch must be authorized. GIT_TOKEN is optional for public repositories.\n * @capability git.fetch { path: string, remoteName: $remote, remote: string, branch: string }\n */"),
277        },
278    );
279    methods.insert(
280        "pull".into(),
281        MethodSig {
282            optional: false,
283            generics: vec![],
284            params: vec![
285                default_string("remote", "origin"),
286                default_string("branch", ""),
287            ],
288            ret: object(
289                &[("previous", Type::String), ("current", Type::String)],
290                false,
291            ),
292            predicate: None,
293            doc: crate::doc(FileId::GIT, "/** Fetch and fast-forward the current branch under git.fetch. Divergence and dirty worktrees are refused.\n * @capability git.fetch { path: string, remoteName: $remote, remote: string, branch: string }\n */"),
294        },
295    );
296    defs.types.insert(
297        "Repository".into(),
298        TypeSymbol {
299            name: "Repository".into(),
300            mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Repository"),
301            declaration_span: Span::at(FileId::GIT),
302            kind: TypeKind::Class {
303                generics: vec![],
304                methods,
305                fields: BTreeMap::from([("path".into(), FieldSig {
306                    ty: Type::String, visibility: Visibility::Private, readonly: true,
307                    optional: false, doc: None,
308                })]),
309                narrowing_checks: BTreeMap::new(),
310                method_visibility: BTreeMap::new(),
311                accessors: vec![],
312                constructor: vec![string("path")],
313                constructor_visibility: crate::Visibility::Public,
314                statics,
315                static_visibility: BTreeMap::new(),
316                static_fields: BTreeMap::new(),
317                extends: None,
318                implements: vec![],
319                doc: crate::doc(
320                    FileId::GIT,
321                    "/** A repository under the VFS root. Construct to open an existing repository, or use Repository.open, Repository.init or Repository.clone. */",
322                ),
323            },
324        },
325    );
326}
327
328fn object(fields: &[(&str, Type)], optional: bool) -> Type {
329    Type::Object {
330        index: None,
331        fields: fields
332            .iter()
333            .map(|(name, ty)| {
334                (
335                    (*name).to_owned(),
336                    if optional {
337                        ObjectField::optional(ty.clone())
338                    } else {
339                        ObjectField::required(ty.clone())
340                    },
341                )
342            })
343            .collect(),
344    }
345}
346
347/// `ty` with an optional member the host leaves out when it has no value.
348fn with_optional(mut ty: Type, name: &str, field: Type) -> Type {
349    if let Type::Object { fields, .. } = &mut ty {
350        fields.insert(name.to_owned(), ObjectField::optional(field));
351    }
352    ty
353}
354
355fn array(ty: Type) -> Type {
356    Type::Array(Box::new(ty))
357}
358
359fn string(name: &str) -> Param {
360    Param::new(name, Type::String)
361}
362
363fn default_string(name: &str, value: &str) -> Param {
364    Param::with_default(name, Type::String, DefaultValue::String(value.into()))
365}
366
367fn options(fields: &[(&str, Type)]) -> Param {
368    Param::with_default(
369        "options",
370        Type::union(vec![object(fields, true), Type::Undefined]),
371        DefaultValue::Undefined,
372    )
373}
374
375fn repository_type() -> Type {
376    Type::ClassRef {
377        mangled: crate::mangle::package_symbol(MODULE_NAME, "Repository"),
378        package: crate::Package(MODULE_NAME.into()),
379        name: "Repository".into(),
380        args: vec![],
381    }
382}