dk-engine 0.3.0

dkod code analysis engine — semantic parsing, indexing, and search
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
416
417
418
use std::path::Path;

use dk_core::{Error, Result};

/// A wrapper around `gix::Repository` providing a simplified interface
/// for Git repository operations.
pub struct GitRepository {
    inner: gix::Repository,
}

impl GitRepository {
    /// Initialize a new Git repository at the given path.
    ///
    /// Creates the directory (and parents) if it does not exist, then
    /// initializes a standard (non-bare) Git repository with a `.git` directory.
    pub fn init(path: &Path) -> Result<Self> {
        std::fs::create_dir_all(path).map_err(|e| {
            Error::Git(format!("failed to create directory {}: {}", path.display(), e))
        })?;

        let repo = gix::init(path).map_err(|e| {
            Error::Git(format!("failed to init repository at {}: {}", path.display(), e))
        })?;

        Ok(Self { inner: repo })
    }

    /// Open an existing Git repository at the given path.
    pub fn open(path: &Path) -> Result<Self> {
        let repo = gix::open(path).map_err(|e| {
            Error::Git(format!("failed to open repository at {}: {}", path.display(), e))
        })?;

        Ok(Self { inner: repo })
    }

    /// Get the working directory path of the repository.
    ///
    /// Returns the working tree directory if available, otherwise falls back
    /// to the `.git` directory itself.
    pub fn path(&self) -> &Path {
        self.inner
            .workdir()
            .unwrap_or_else(|| self.inner.git_dir())
    }

    /// Get a reference to the inner `gix::Repository`.
    pub fn inner(&self) -> &gix::Repository {
        &self.inner
    }

    /// Create a Git commit with the current working directory state.
    /// Uses command-line git for simplicity (gix's commit API is complex).
    pub fn commit(&self, message: &str, author_name: &str, author_email: &str) -> Result<String> {
        let workdir = self.path();

        // Stage all files
        let output = std::process::Command::new("git")
            .args(["add", "-A"])
            .current_dir(workdir)
            .output()
            .map_err(|e| Error::Git(format!("git add failed: {e}")))?;

        if !output.status.success() {
            return Err(Error::Git(format!(
                "git add failed: {}",
                String::from_utf8_lossy(&output.stderr)
            )));
        }

        let output = std::process::Command::new("git")
            .args([
                "commit",
                "--allow-empty",
                "-m", message,
                "--author", &format!("{} <{}>", author_name, author_email),
            ])
            .current_dir(workdir)
            .output()
            .map_err(|e| Error::Git(format!("git commit failed: {e}")))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            if stderr.contains("nothing to commit") {
                return self.head_hash()?
                    .ok_or_else(|| Error::Git("no HEAD after commit".into()));
            }
            return Err(Error::Git(format!("git commit failed: {stderr}")));
        }

        self.head_hash()?
            .ok_or_else(|| Error::Git("no HEAD after commit".into()))
    }

    // ── NSI: Tree-based read/write operations ──────────────────────────

    /// Read a file's content from a specific commit's tree (NOT the working directory).
    /// This is the core isolation primitive for Native Session Isolation.
    ///
    /// `commit_hex` — hex SHA of the commit whose tree to read from.
    /// `path` — relative file path within the tree (e.g. "src/main.rs").
    ///
    /// Returns the raw bytes of the blob, or an error if the commit / path
    /// does not exist or the entry is not a blob.
    pub fn read_tree_entry(&self, commit_hex: &str, path: &str) -> Result<Vec<u8>> {
        let oid = gix::ObjectId::from_hex(commit_hex.as_bytes())
            .map_err(|e| Error::Git(format!("invalid commit hex '{commit_hex}': {e}")))?;

        let commit = self
            .inner
            .find_commit(oid)
            .map_err(|e| Error::Git(format!("failed to find commit {commit_hex}: {e}")))?;

        let tree = self
            .inner
            .find_tree(commit.tree_id().expect("commit always has tree"))
            .map_err(|e| Error::Git(format!("failed to find tree for commit {commit_hex}: {e}")))?;

        let entry = tree
            .lookup_entry_by_path(path)
            .map_err(|e| Error::Git(format!("failed to lookup '{path}' in {commit_hex}: {e}")))?
            .ok_or_else(|| Error::Git(format!("path '{path}' not found in commit {commit_hex}")))?;

        let object = entry
            .object()
            .map_err(|e| Error::Git(format!("failed to read object for '{path}': {e}")))?;

        if object.kind != gix::object::Kind::Blob {
            return Err(Error::Git(format!(
                "path '{path}' in commit {commit_hex} is not a blob (is {:?})",
                object.kind
            )));
        }

        Ok(object.data.clone())
    }

    /// List all files (recursive) in a commit's tree. Returns relative paths
    /// using forward-slash separators.
    ///
    /// Only non-tree entries (blobs, symlinks, submodules) are included.
    pub fn list_tree_files(&self, commit_hex: &str) -> Result<Vec<String>> {
        let oid = gix::ObjectId::from_hex(commit_hex.as_bytes())
            .map_err(|e| Error::Git(format!("invalid commit hex '{commit_hex}': {e}")))?;

        let commit = self
            .inner
            .find_commit(oid)
            .map_err(|e| Error::Git(format!("failed to find commit {commit_hex}: {e}")))?;

        let tree = self
            .inner
            .find_tree(commit.tree_id().expect("commit always has tree"))
            .map_err(|e| Error::Git(format!("failed to find tree for commit {commit_hex}: {e}")))?;

        let entries = tree
            .traverse()
            .breadthfirst
            .files()
            .map_err(|e| Error::Git(format!("tree traversal failed for {commit_hex}: {e}")))?;

        let paths = entries
            .into_iter()
            .filter(|e| !e.mode.is_tree())
            .map(|e| e.filepath.to_string())
            .collect();

        Ok(paths)
    }

    /// Build a new git tree by applying overlay changes on a base tree, create
    /// a commit, and update the working directory.
    ///
    /// For each overlay entry:
    /// - `Some(content)` → upsert a blob at that path
    /// - `None` → delete the entry at that path
    ///
    /// After committing, the working directory is updated via `git checkout HEAD -- .`.
    ///
    /// Returns the hex SHA of the new commit.
    pub fn commit_tree_overlay(
        &self,
        base_commit_hex: &str,
        overlay: &[(String, Option<Vec<u8>>)],
        parent_commit_hex: &str,
        message: &str,
        author_name: &str,
        author_email: &str,
    ) -> Result<String> {
        use gix::object::tree::EntryKind;

        // Parse the base commit to get its tree
        let base_oid = gix::ObjectId::from_hex(base_commit_hex.as_bytes())
            .map_err(|e| Error::Git(format!("invalid base commit hex '{base_commit_hex}': {e}")))?;

        let base_commit = self
            .inner
            .find_commit(base_oid)
            .map_err(|e| Error::Git(format!("failed to find base commit {base_commit_hex}: {e}")))?;

        let base_tree = self
            .inner
            .find_tree(base_commit.tree_id().expect("commit always has tree"))
            .map_err(|e| Error::Git(format!("failed to find base tree: {e}")))?;

        // Parse the parent commit
        let parent_oid = gix::ObjectId::from_hex(parent_commit_hex.as_bytes())
            .map_err(|e| Error::Git(format!("invalid parent commit hex '{parent_commit_hex}': {e}")))?;

        // Create a tree editor from the base tree
        let mut editor = self
            .inner
            .edit_tree(base_tree.id)
            .map_err(|e| Error::Git(format!("failed to create tree editor: {e}")))?;

        // Apply each overlay entry
        for (path, maybe_content) in overlay {
            match maybe_content {
                Some(content) => {
                    // Write the blob to the object database
                    let blob_id = self
                        .inner
                        .write_blob(content)
                        .map_err(|e| Error::Git(format!("failed to write blob for '{path}': {e}")))?;

                    // Upsert into the tree — detect executable by file extension heuristic
                    // (default to regular blob)
                    editor
                        .upsert(path.as_str(), EntryKind::Blob, blob_id.detach())
                        .map_err(|e| Error::Git(format!("failed to upsert '{path}': {e}")))?;
                }
                None => {
                    // Remove the entry from the tree
                    editor
                        .remove(path.as_str())
                        .map_err(|e| Error::Git(format!("failed to remove '{path}': {e}")))?;
                }
            }
        }

        // Write the modified tree to the object database
        let new_tree_id = editor
            .write()
            .map_err(|e| Error::Git(format!("failed to write edited tree: {e}")))?;

        // Build the commit object with explicit author/committer
        let now_secs = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs() as i64;

        let time = gix::date::Time {
            seconds: now_secs,
            offset: 0,
        };

        let sig = gix::actor::Signature {
            name: author_name.into(),
            email: author_email.into(),
            time,
        };

        let mut time_buf = gix::date::parse::TimeBuf::default();
        let sig_ref = sig.to_ref(&mut time_buf);

        let commit_id = self
            .inner
            .commit_as(sig_ref, sig_ref, "HEAD", message, new_tree_id.detach(), [parent_oid])
            .map_err(|e| Error::Git(format!("failed to create commit: {e}")))?;

        let commit_hex = commit_id.to_hex().to_string();

        // Update the working directory to match the new commit.
        // Spawn on a separate thread to avoid blocking the tokio async runtime
        // when this function is called from an async context via spawn_blocking.
        let work_dir = self.path().to_path_buf();
        let output = std::thread::spawn(move || {
            std::process::Command::new("git")
                .args(["checkout", "HEAD", "--", "."])
                .current_dir(&work_dir)
                .output()
        })
        .join()
        .map_err(|_| Error::Git("git checkout thread panicked".into()))?
        .map_err(|e| Error::Git(format!("git checkout failed: {e}")))?;

        if !output.status.success() {
            // Non-fatal: the commit succeeded, the working directory just wasn't updated.
            // Log but don't fail.
            tracing::warn!(
                "git checkout HEAD -- . failed after commit: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }

        Ok(commit_hex)
    }

    /// Create an orphan commit from overlay entries on an empty repository
    /// (no existing commits). This is used for the very first commit.
    ///
    /// Builds a tree from scratch using only the overlay entries (ignoring
    /// `None`/deletion entries since there's nothing to delete), creates a
    /// root commit with no parents, and updates HEAD.
    ///
    /// Returns the hex SHA of the new commit.
    pub fn commit_initial_overlay(
        &self,
        overlay: &[(String, Option<Vec<u8>>)],
        message: &str,
        author_name: &str,
        author_email: &str,
    ) -> Result<String> {
        use gix::object::tree::EntryKind;

        // Start from an empty tree and build up the initial file tree.
        let empty_tree = self
            .inner
            .empty_tree();

        let mut editor = self
            .inner
            .edit_tree(empty_tree.id)
            .map_err(|e| Error::Git(format!("failed to create tree editor: {e}")))?;

        // Apply overlay entries (only additions, skip deletions)
        for (path, maybe_content) in overlay {
            if let Some(content) = maybe_content {
                let blob_id = self
                    .inner
                    .write_blob(content)
                    .map_err(|e| Error::Git(format!("failed to write blob for '{path}': {e}")))?;

                editor
                    .upsert(path.as_str(), EntryKind::Blob, blob_id.detach())
                    .map_err(|e| Error::Git(format!("failed to upsert '{path}': {e}")))?;
            }
        }

        let new_tree_id = editor
            .write()
            .map_err(|e| Error::Git(format!("failed to write initial tree: {e}")))?;

        // Build the commit with no parents (orphan/root commit)
        let now_secs = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs() as i64;

        let time = gix::date::Time {
            seconds: now_secs,
            offset: 0,
        };

        let sig = gix::actor::Signature {
            name: author_name.into(),
            email: author_email.into(),
            time,
        };

        let mut time_buf = gix::date::parse::TimeBuf::default();
        let sig_ref = sig.to_ref(&mut time_buf);

        // Root commit: no parents (empty iterator)
        let commit_id = self
            .inner
            .commit_as(
                sig_ref,
                sig_ref,
                "HEAD",
                message,
                new_tree_id.detach(),
                std::iter::empty::<gix::ObjectId>(),
            )
            .map_err(|e| Error::Git(format!("failed to create initial commit: {e}")))?;

        let commit_hex = commit_id.to_hex().to_string();

        // Update working directory
        let work_dir = self.path().to_path_buf();
        let output = std::thread::spawn(move || {
            std::process::Command::new("git")
                .args(["checkout", "HEAD", "--", "."])
                .current_dir(&work_dir)
                .output()
        })
        .join()
        .map_err(|_| Error::Git("git checkout thread panicked".into()))?
        .map_err(|e| Error::Git(format!("git checkout failed: {e}")))?;

        if !output.status.success() {
            tracing::warn!(
                "git checkout HEAD -- . failed after initial commit: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }

        Ok(commit_hex)
    }

    /// Get the HEAD commit hash as a hex string, or `None` if the repository
    /// is empty (no commits yet).
    pub fn head_hash(&self) -> Result<Option<String>> {
        let head = self
            .inner
            .head()
            .map_err(|e| Error::Git(format!("failed to get HEAD: {}", e)))?;

        if head.is_unborn() {
            return Ok(None);
        }

        match head.into_peeled_id() {
            Ok(id) => Ok(Some(id.to_hex().to_string())),
            Err(e) => Err(Error::Git(format!("failed to peel HEAD: {}", e))),
        }
    }
}