moon_pdk_api 2.1.3

Core APIs for creating moon WASM plugins.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Serializable interface for source-control provider plugins.
//!
//! # Lifecycle
//!
//! A host registers a provider, then moon calls `initialize_vcs` once when it
//! creates the VCS client for a command. Initialization reports whether the
//! provider applies to the workspace and, when it does, pins the repository
//! state before any impact or hook query. A new initialization requires a new
//! plugin instance.
//!
//! The provider retains any opaque state needed to pin the initialized state.
//! Every later operation must answer from that state, including after a cache miss;
//! it must not silently refresh the working copy or re-resolve movable labels
//! against newer repository state.

use crate::{Id, MoonContext, VirtualPath};
use bitflags::bitflags;
use std::collections::BTreeMap;
use std::path::PathBuf;
use warpgate_api::{api_enum, api_struct, api_unit_enum};

/// Exact wire-protocol generation supported by this host or provider.
///
/// VCS plugins use lockstep protocol generations. Adding fields without safe
/// serde defaults, changing lifecycle semantics, or assigning new change-mask
/// bits requires incrementing this version.
pub const VCS_PLUGIN_PROTOCOL_VERSION: u16 = 6;

api_struct!(
    /// Input passed to `register_vcs` before any other provider operation.
    pub struct RegisterVcsInput {
        /// ID under which the host loaded this plugin instance.
        pub id: Id,
        /// Exact protocol generation required by the host.
        pub host_protocol_version: u16,
    }
);

api_struct!(
    /// Provider metadata returned by `register_vcs`.
    #[serde(default)]
    pub struct RegisterVcsOutput {
        /// Human-readable provider name.
        pub name: String,
        /// Optional human-readable provider description.
        pub description: Option<String>,
        /// Version of the provider implementation, independent of the protocol.
        pub plugin_version: String,
        /// Exact protocol generation implemented by the provider.
        pub protocol_version: u16,
    }
);

api_struct!(
    /// Repository roots fixed by `initialize_vcs`.
    pub struct VcsRoots {
        /// Root of the repository metadata.
        pub repository_root: VirtualPath,
        /// Root of the active worktree or working copy.
        pub working_root: VirtualPath,
    }
);

api_struct!(
    /// Input passed to `initialize_vcs` exactly once per plugin instance.
    pub struct InitializeVcsInput {
        /// Movable provider expression to resolve and pin as the baseline.
        pub baseline: Option<String>,
        /// Preferred remote names, in priority order, for repository metadata.
        #[serde(default)]
        pub remote_candidates: Vec<String>,
        pub context: MoonContext,
    }
);

api_struct!(
    /// An exact provider state resolved during initialization.
    pub struct VcsState {
        /// Exact state identity, or `None` when the repository has no recorded
        /// state yet, such as an unborn Git repository.
        ///
        /// The ID must remain stable and round-trippable to the provider for the
        /// lifetime of this initialization. It need not survive rewritten history
        /// or a later plugin instance.
        pub id: Option<String>,
        /// Human-readable bookmark, branch, channel, change, or equivalent.
        /// Labels may move and must not be used as exact state identities.
        pub label: Option<String>,
    }
);

api_unit_enum!(
    /// Availability of repository history at initialization.
    pub enum VcsHistoryCompleteness {
        /// All history required for comparisons is available.
        Complete,
        /// History is known to be incomplete, such as in a shallow clone.
        Incomplete,
        /// The provider cannot determine whether history is complete.
        #[default]
        Unknown,
    }
);

api_struct!(
    /// Provider metadata and exact states pinned by `initialize_vcs`.
    pub struct VcsInitialization {
        /// Stable VCS client kind, such as `git` or `jj`.
        pub client: Id,
        /// Version of the source-control client used by the provider.
        pub client_version: Option<String>,
        /// Repository roots fixed by this initialization.
        pub roots: VcsRoots,
        /// Current state as it existed during initialization.
        pub current: VcsState,
        /// Current recorded state, excluding working changes.
        ///
        /// This may equal `current`, as in Git, or identify its recorded parent,
        /// as with a Jujutsu working-copy commit. Providers may synthesize an
        /// initialization-scoped state when multiple parents must be represented.
        pub recorded: VcsState,
        /// Baseline resolved from `InitializeVcsInput::baseline`, when available.
        pub baseline: Option<VcsState>,
        pub repository_slug: Option<String>,
        pub history: VcsHistoryCompleteness,
    }
);

api_enum!(
    /// Applicability and initialized state returned by `initialize_vcs`.
    #[serde(tag = "status", rename_all = "kebab-case")]
    pub enum InitializeVcsOutput {
        /// The provider does not apply to this workspace.
        NotDetected {
            /// Human-readable explanation of the detection result.
            reason: String,
        },
        /// The provider applies and has pinned its state for this command.
        Initialized {
            initialization: Box<VcsInitialization>,
        },
    }
);

api_enum!(
    /// moon-level reason for requesting impacted files.
    #[derive(Default)]
    #[serde(tag = "type", rename_all = "kebab-case")]
    pub enum VcsImpactIntent {
        /// Working changes captured during initialization.
        #[default]
        Working,
        /// Changes introduced by `head` since it diverged from `base`.
        ///
        /// When `base` is absent, compare `head` with its provider-defined
        /// predecessor. When `head` is absent, use the initialized recorded state.
        /// Movable expressions must be resolved from the initialized state.
        Submission {
            base: Option<String>,
            head: Option<String>,
            /// Include working changes captured during initialization.
            include_working: bool,
        },
    }
);

api_struct!(
    /// Input passed to `get_vcs_impacts` after initialization.
    pub struct GetVcsImpactsInput {
        /// Must be identical to the initialization context.
        pub context: MoonContext,
        pub intent: VcsImpactIntent,
    }
);

#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "schematic", derive(schematic::Schematic))]
#[serde(transparent)]
pub struct VcsChangeMask(u8);

bitflags! {
    /// Compact set of change-kind and location flags for one path.
    ///
    /// A valid mask contains at least one change-kind bit and at least one
    /// location bit. Unassigned bits are reserved: receiving one is a protocol
    /// error, and assigning a new bit requires a protocol-version increment.
    impl VcsChangeMask: u8 {
    /// The path was added.
    const ADDED = 1;
    /// The path was deleted.
    const DELETED = 2;
    /// The path was modified.
    const MODIFIED = 4;
    /// The path changed in recorded history.
    const RECORDED = 8;
    /// The path changed in a staging area.
    const STAGED = 16;
    /// The path changed in the working copy.
    const WORKING = 32;
    /// The path is not tracked by the provider.
    const UNTRACKED = 64;

    const CHANGE_BITS = Self::ADDED.bits() | Self::DELETED.bits() | Self::MODIFIED.bits();
    const LOCATION_BITS = Self::RECORDED.bits() | Self::STAGED.bits() | Self::WORKING.bits() | Self::UNTRACKED.bits();
    const KNOWN_BITS = Self::CHANGE_BITS.bits() | Self::LOCATION_BITS.bits();
    }
}

api_unit_enum!(
    /// Safety guarantee attached to an impact result.
    pub enum VcsImpactCompleteness {
        /// Every impacted path and applicable mask bit is present exactly.
        Exact,
        /// Every possibly impacted path and mask bit is present, but the result
        /// may contain false positives. False negatives are forbidden.
        Conservative,
        /// The provider could not produce a safe answer.
        #[default]
        Unavailable,
    }
);

api_struct!(
    /// Output returned by `get_vcs_impacts`.
    #[serde(default)]
    pub struct GetVcsImpactsOutput {
        /// Canonical UTF-8 file paths relative to the moon workspace root.
        ///
        /// Keys use `/` separators and must be non-empty, must not contain `.`,
        /// `..`, empty components, backslashes, NULs, or names invalid on the
        /// host platform, and must not be absolute.
        pub changes: BTreeMap<PathBuf, VcsChangeMask>,
        pub completeness: VcsImpactCompleteness,
        /// Human-readable explanations for degraded or unavailable results.
        pub diagnostics: Vec<String>,
    }
);

api_struct!(
    /// Input passed to `setup_vcs_hook_environment` after initialization.
    pub struct SetupVcsHookEnvironmentInput {
        /// Must be identical to the initialization context.
        pub context: MoonContext,
        /// Canonical path to moon's hooks directory.
        pub hooks_dir: VirtualPath,
        /// Provider-native hook names that moon intends to install.
        pub hooks: Vec<String>,
    }
);

api_struct!(
    /// Hook execution environment returned by `setup_vcs_hook_environment`.
    #[serde(default)]
    pub struct SetupVcsHookEnvironmentOutput {
        /// Working directory in which moon should execute installed hooks.
        pub working_dir: Option<VirtualPath>,
    }
);

api_struct!(
    /// Input passed to `teardown_vcs_hook_environment` after initialization.
    pub struct TeardownVcsHookEnvironmentInput {
        /// Must be identical to the initialization context.
        pub context: MoonContext,
        /// Canonical path to moon's hooks directory.
        pub hooks_dir: VirtualPath,
        /// Provider-native hook names previously managed by moon.
        pub hooks: Vec<String>,
    }
);

api_struct!(
    /// Output returned by `teardown_vcs_hook_environment`.
    pub struct TeardownVcsHookEnvironmentOutput {}
);

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serializes_working_intents() {
        assert_eq!(
            serde_json::to_value(VcsImpactIntent::Working).unwrap(),
            serde_json::json!({"type": "working"})
        );
        assert_eq!(
            serde_json::to_value(VcsImpactIntent::Submission {
                base: None,
                head: None,
                include_working: true,
            })
            .unwrap(),
            serde_json::json!({
                "type": "submission",
                "base": null,
                "head": null,
                "include_working": true,
            })
        );
    }

    #[test]
    fn serializes_vcs_identifiers_as_strings() {
        let initialization = VcsInitialization {
            client: Id::raw("git"),
            client_version: Some("2.0.0".into()),
            roots: VcsRoots {
                repository_root: VirtualPath::new("/repo/.git"),
                working_root: VirtualPath::new("/repo"),
            },
            current: VcsState {
                id: Some("abc123".into()),
                label: Some("main".into()),
            },
            recorded: VcsState {
                id: None,
                label: None,
            },
            baseline: None,
            repository_slug: None,
            history: VcsHistoryCompleteness::Complete,
        };
        let output = InitializeVcsOutput::Initialized {
            initialization: Box::new(initialization),
        };

        let value = serde_json::to_value(output).unwrap();

        assert_eq!(value["status"], serde_json::json!("initialized"));
        assert_eq!(value["initialization"]["client"], serde_json::json!("git"));
        assert_eq!(
            value["initialization"]["current"]["id"],
            serde_json::json!("abc123")
        );
        assert_eq!(
            value["initialization"]["recorded"]["id"],
            serde_json::Value::Null
        );
        assert_eq!(
            serde_json::to_value("main").unwrap(),
            serde_json::json!("main")
        );
    }

    #[test]
    fn serializes_change_masks_as_numbers() {
        assert_eq!(
            serde_json::to_value(VcsChangeMask::ADDED).unwrap(),
            serde_json::json!(1)
        );
        assert_eq!(
            serde_json::to_value(VcsChangeMask::DELETED).unwrap(),
            serde_json::json!(2)
        );
        assert_eq!(
            serde_json::to_value(VcsChangeMask::MODIFIED).unwrap(),
            serde_json::json!(4)
        );
        assert_eq!(
            serde_json::to_value(VcsChangeMask::RECORDED).unwrap(),
            serde_json::json!(8)
        );
        assert_eq!(
            serde_json::to_value(VcsChangeMask::STAGED).unwrap(),
            serde_json::json!(16)
        );
        assert_eq!(
            serde_json::to_value(VcsChangeMask::WORKING).unwrap(),
            serde_json::json!(32)
        );
        assert_eq!(
            serde_json::to_value(VcsChangeMask::UNTRACKED).unwrap(),
            serde_json::json!(64)
        );

        let output = GetVcsImpactsOutput {
            changes: BTreeMap::from([
                (
                    PathBuf::from("a.txt"),
                    VcsChangeMask::ADDED | VcsChangeMask::WORKING,
                ),
                (
                    PathBuf::from("z.txt"),
                    VcsChangeMask::MODIFIED | VcsChangeMask::RECORDED,
                ),
            ]),
            completeness: VcsImpactCompleteness::Exact,
            ..Default::default()
        };

        assert_eq!(
            serde_json::to_value(output).unwrap(),
            serde_json::json!({
                "changes": {
                    "a.txt": 33,
                    "z.txt": 12,
                },
                "completeness": "exact",
                "diagnostics": [],
            })
        );
    }

    #[test]
    fn defaults_missing_impact_completeness_to_unavailable() {
        let output: GetVcsImpactsOutput = serde_json::from_value(serde_json::json!({
            "changes": {},
            "diagnostics": [],
        }))
        .unwrap();

        assert_eq!(output.completeness, VcsImpactCompleteness::Unavailable);
    }
}