tiny.smg.storage.store
Defined in storage.
API (13)
Actions
Public operations.
Publisher.closePublisher.finishReadStore.closeStore.closebeginPublishbeginRepairPublishopenopenMutationopenReadtryOpenRead: Opens the existing graph without waiting for a publisher or repairing storage.
Types and contracts
Public types and contracts.
Source
Source: tools/smg/src/storage/root.zig:10
zig
pub const store = @import("store.zig");Source: tools/smg/src/storage/store.zig
zig
const std = @import("std");const sql = @import("sql");const sys = @import("sys");const smg = @import("../root.zig");const storage = smg.storage;const database = storage.database;const paths = storage.paths;const sync = storage.sync;const text = smg.text;const fs_io = std.Options.debug_io;const gitignore_content = paths.database_file_name ++ "\n" ++ paths.wal_file_name ++ "\nnames.bin\nnames.bin.tmp\nsearch.sql\nsearch.sql.wal\nsearch.sql.tmp\nsearch.sql.tmp.wal\nsummary.json\nsummary.json.tmp\n" ++ paths.lock_file_name ++ "\n" ++ paths.refresh_file_name ++ "\n" ++ paths.source_file_name ++ "\n" ++ paths.source_temp_file_name ++ ".*\n";pub const Store = struct { allocator: std.mem.Allocator, dir_path: []const u8, lock: std.Io.File, database: database.Database, pub fn close(self: *Store) void { self.database.deinit(); releaseLock(&self.lock); self.allocator.free(self.dir_path); self.* = undefined; }};pub const ReadStore = struct { allocator: std.mem.Allocator, dir_path: []const u8, lock: std.Io.File, reader: database.Reader, pub fn close(self: *ReadStore) void { self.reader.deinit(); releaseLock(&self.lock); self.allocator.free(self.dir_path); self.* = undefined; }};pub const Publisher = struct { store: Store, root: []const u8, pub fn finish(self: *Publisher) !void { try sync.finishRefresh(self.store.allocator, self.root); } pub fn close(self: *Publisher) void { self.store.close(); self.* = undefined; }};pub fn open(allocator: std.mem.Allocator, root: []const u8, limits: smg.StorageLimits) !Store { return try openWriter(allocator, root, limits, false);}pub fn openMutation(allocator: std.mem.Allocator, root: []const u8, limits: smg.StorageLimits) !Store { return try openWriter(allocator, root, limits, true);}pub fn beginRepairPublish(allocator: std.mem.Allocator, root: []const u8, limits: smg.StorageLimits) !Publisher { var opened = try openWriter(allocator, root, limits, false); errdefer opened.close(); try sync.beginRefresh(allocator, root); return .{ .store = opened, .root = root };}pub fn beginPublish( allocator: std.mem.Allocator, root: []const u8, expected_head: sql.Hash, limits: smg.StorageLimits,) !Publisher { var opened = try openWriter(allocator, root, limits, true); errdefer opened.close(); const current_head = (try opened.database.connection.checkout()).head; if (!sql.version.same(current_head, expected_head)) return error.StaleSourceHead; try sync.beginRefresh(allocator, root); return .{ .store = opened, .root = root };}fn openWriter( allocator: std.mem.Allocator, root: []const u8, limits: smg.StorageLimits, require_complete: bool,) !Store { const dir_path = try openDirPath(allocator, root); errdefer allocator.free(dir_path); var lock = try acquireExclusiveLock(allocator, dir_path); errdefer releaseLock(&lock); if (require_complete and try sync.refreshIncomplete(allocator, root)) return error.GraphRefreshIncomplete; try ensureGitignore(allocator, dir_path, limits); var dir = try std.Io.Dir.openDirAbsolute(fs_io, dir_path, .{}); defer dir.close(fs_io); var opened = try database.open(allocator, dir, limits); errdefer opened.deinit(); try database.checkpointIfWalLarge(&opened); return .{ .allocator = allocator, .dir_path = dir_path, .lock = lock, .database = opened, };}pub fn openRead(allocator: std.mem.Allocator, root: []const u8, limits: smg.StorageLimits) !ReadStore { return readStore(allocator, root, limits, true);}/// Opens the existing graph without waiting for a publisher or repairing storage.pub fn tryOpenRead( allocator: std.mem.Allocator, root: []const u8, limits: smg.StorageLimits,) !ReadStore { return readStore(allocator, root, limits, false);}fn readStore( allocator: std.mem.Allocator, root: []const u8, limits: smg.StorageLimits, wait: bool,) !ReadStore { const dir_path = try existingDirPath(allocator, root); errdefer allocator.free(dir_path); var lock = try sharedLock(allocator, dir_path, wait); errdefer releaseLock(&lock); if (try sync.refreshIncomplete(allocator, root)) return error.GraphRefreshIncomplete; var dir = try std.Io.Dir.openDirAbsolute(fs_io, dir_path, .{}); defer dir.close(fs_io); var reader = switch (try database.openReader(allocator, dir, limits)) { .ready => |ready| ready, .repair_required => |reason| return switch (reason) { .stale_refs, .root_mismatch => error.GraphRefreshIncomplete, .file => |file_reason| switch (file_reason) { .wal_too_large => error.WalRecoveryLimitBytesExceeded, else => error.GraphStoreRepairRequired, }, .missing_refs, .invalid_refs, .missing_branch => error.GraphStoreRepairRequired, }, }; errdefer reader.deinit(); if (!try database.readerSchemaReady(&reader)) return error.GraphStoreMigrationRequired; return .{ .allocator = allocator, .dir_path = dir_path, .lock = lock, .reader = reader, };}fn existingDirPath(allocator: std.mem.Allocator, root: []const u8) ![]const u8 { return try std.fs.path.join(allocator, &.{ root, paths.smg_dir_name });}fn openDirPath(allocator: std.mem.Allocator, root: []const u8) ![]const u8 { const dir_path = try std.fs.path.join(allocator, &.{ root, paths.smg_dir_name }); errdefer allocator.free(dir_path); try sys.fs.createDirPath(dir_path); return dir_path;}fn acquireExclusiveLock(allocator: std.mem.Allocator, dir_path: []const u8) !std.Io.File { const lock_path = try std.fs.path.join(allocator, &.{ dir_path, paths.lock_file_name }); defer allocator.free(lock_path); var lock = try std.Io.Dir.createFileAbsolute(fs_io, lock_path, .{ .truncate = false, .read = true, .permissions = @fromBackingInt(@intCast(0o600)), }); errdefer lock.close(fs_io); try lock.lock(fs_io, .exclusive); try validateLock(lock, lock_path); return lock;}fn sharedLock(allocator: std.mem.Allocator, dir_path: []const u8, wait: bool) !std.Io.File { const lock_path = try std.fs.path.join(allocator, &.{ dir_path, paths.lock_file_name }); defer allocator.free(lock_path); var lock = std.Io.Dir.openFileAbsolute(fs_io, lock_path, .{}) catch |err| switch (err) { error.FileNotFound => return error.NoProject, else => return err, }; errdefer lock.close(fs_io); if (wait) { try lock.lock(fs_io, .shared); } else if (!try lock.tryLock(fs_io, .shared)) return error.GraphStoreBusy; try validateLock(lock, lock_path); return lock;}fn validateLock(lock: std.Io.File, lock_path: []const u8) !void { const current = std.Io.Dir.openFileAbsolute(fs_io, lock_path, .{}) catch |err| switch (err) { error.FileNotFound => return error.GraphStoreBusy, else => return err, }; defer current.close(fs_io); const held_identity = try sys.fd.identity(lock.handle); const current_identity = try sys.fd.identity(current.handle); if (!std.meta.eql(held_identity, current_identity)) return error.GraphStoreBusy;}fn releaseLock(lock: *std.Io.File) void { lock.unlock(fs_io); lock.close(fs_io);}fn ensureGitignore( allocator: std.mem.Allocator, dir_path: []const u8, limits: smg.StorageLimits,) !void { const path = try std.fs.path.join(allocator, &.{ dir_path, ".gitignore" }); defer allocator.free(path); if (sys.fs.readFileAlloc(allocator, path, limits.max_gitignore_bytes)) |existing| { defer allocator.free(existing); if (std.mem.eql(u8, existing, gitignore_content)) return; } else |_| {} try text.writeFile(path, gitignore_content);}test "store open creates directory gitignore and lock" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 0); defer sys.fs.deleteTree(root) catch {}; var opened = try open(allocator, root, smg.default_limits.storage); const dir_path = try allocator.dupe(u8, opened.dir_path); opened.close(); const gitignore_path = try std.fs.path.join(allocator, &.{ dir_path, ".gitignore" }); const gitignore = try sys.fs.readFileAlloc(allocator, gitignore_path, 4096); try std.testing.expectEqualStrings(gitignore_content, gitignore); try std.testing.expect(std.mem.indexOf(u8, gitignore, "search.sql.version") == null); const lock_path = try std.fs.path.join(allocator, &.{ dir_path, paths.lock_file_name }); try std.testing.expect(sys.fs.exists(lock_path));}test "read store opens after writer closes" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 1); defer sys.fs.deleteTree(root) catch {}; var writer = try open(allocator, root, smg.default_limits.storage); writer.close(); var reader = try openRead(allocator, root, smg.default_limits.storage); reader.close();}test "read store rejects an incomplete graph refresh until a writer completes it" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 2); defer sys.fs.deleteTree(root) catch {}; var writer = try open(allocator, root, smg.default_limits.storage); writer.close(); var interrupted = try beginRepairPublish(allocator, root, smg.default_limits.storage); interrupted.close(); try std.testing.expectError( error.GraphRefreshIncomplete, openRead(allocator, root, smg.default_limits.storage), ); try std.testing.expectError( error.GraphRefreshIncomplete, openMutation(allocator, root, smg.default_limits.storage), ); var publisher = try beginRepairPublish(allocator, root, smg.default_limits.storage); errdefer publisher.close(); try publisher.finish(); publisher.close(); var reader = try openRead(allocator, root, smg.default_limits.storage); reader.close();}test "expected publication rejects stale heads before arming the refresh marker" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 6); defer sys.fs.deleteTree(root) catch {}; var writer = try open(allocator, root, smg.default_limits.storage); const source_head = (try writer.database.connection.checkout()).head; writer.close(); var matching = try beginPublish( allocator, root, source_head, smg.default_limits.storage, ); try std.testing.expect(try sync.refreshIncomplete(allocator, root)); try matching.finish(); matching.close(); try std.testing.expect(!try sync.refreshIncomplete(allocator, root)); var winner = try open(allocator, root, smg.default_limits.storage); try database.edits.apply(&winner.database, .{ .concept_document_puts = &.{.{ .rowid = 1, .name = "winner", .document = "{\"name\":\"winner\"}", }} }); const winner_head = try database.commit(&winner.database); winner.close(); try std.testing.expect(!sql.version.same(source_head, winner_head)); try std.testing.expectError( error.StaleSourceHead, beginPublish(allocator, root, source_head, smg.default_limits.storage), ); try std.testing.expect(!try sync.refreshIncomplete(allocator, root)); var reopened = try open(allocator, root, smg.default_limits.storage); try std.testing.expect(sql.version.same(winner_head, (try reopened.database.connection.checkout()).head)); reopened.close();}test "read store shared locks coexist and exclude a publisher" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 3); defer sys.fs.deleteTree(root) catch {}; var writer = try open(allocator, root, smg.default_limits.storage); writer.close(); const dir_path = try existingDirPath(allocator, root); const lock_path = try std.fs.path.join(allocator, &.{ dir_path, paths.lock_file_name }); { var first = try sharedLock(allocator, dir_path, true); defer releaseLock(&first); var second = try sharedLock(allocator, dir_path, true); defer releaseLock(&second); var exclusive = try std.Io.Dir.openFileAbsolute(fs_io, lock_path, .{}); defer exclusive.close(fs_io); try std.testing.expect(!try exclusive.tryLock(fs_io, .exclusive)); }}test "store lock validation accepts the current shared and exclusive lock" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 7); defer sys.fs.deleteTree(root) catch {}; const dir_path = try openDirPath(allocator, root); const lock_path = try std.fs.path.join(allocator, &.{ dir_path, paths.lock_file_name }); for ([_]bool{ false, true }) |shared| { var lock = if (shared) try sharedLock(allocator, dir_path, false) else try acquireExclusiveLock(allocator, dir_path); defer releaseLock(&lock); try validateLock(lock, lock_path); var contender = try std.Io.Dir.openFileAbsolute(fs_io, lock_path, .{}); defer contender.close(fs_io); try std.testing.expect(!try contender.tryLock(fs_io, .exclusive)); }}test "store lock validation refuses retirement while the replacement remains exclusive" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 8); defer sys.fs.deleteTree(root) catch {}; const dir_path = try openDirPath(allocator, root); const retired_path = try std.fs.path.join(allocator, &.{ root, "retired" }); const lock_path = try std.fs.path.join(allocator, &.{ dir_path, paths.lock_file_name }); for ([_]bool{ false, true }) |shared| { var lock = if (shared) try sharedLock(allocator, dir_path, false) else try acquireExclusiveLock(allocator, dir_path); defer releaseLock(&lock); try sys.fs.rename(dir_path, retired_path); defer sys.fs.deleteTree(retired_path) catch {}; try sys.fs.createDirPath(dir_path); var replacement = try acquireExclusiveLock(allocator, dir_path); defer releaseLock(&replacement); try std.testing.expectError(error.GraphStoreBusy, validateLock(lock, lock_path)); try std.testing.expectError(error.GraphStoreBusy, sharedLock(allocator, dir_path, false)); try validateLock(replacement, lock_path); }}test "store lock validation refuses retirement without creating a replacement" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 9); defer sys.fs.deleteTree(root) catch {}; const dir_path = try openDirPath(allocator, root); const retired_path = try std.fs.path.join(allocator, &.{ root, "retired" }); const lock_path = try std.fs.path.join(allocator, &.{ dir_path, paths.lock_file_name }); var lock = try acquireExclusiveLock(allocator, dir_path); defer releaseLock(&lock); try sys.fs.rename(dir_path, retired_path); try std.testing.expectError(error.GraphStoreBusy, validateLock(lock, lock_path)); try std.testing.expect(!sys.fs.exists(dir_path));}test "read store observation preserves absence and refuses a held writer" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try testRoot(allocator, 10); defer sys.fs.deleteTree(root) catch {}; try std.testing.expectError(error.NoProject, tryOpenRead(allocator, root, smg.default_limits.storage)); try std.testing.expectError(error.NoProject, openRead(allocator, root, smg.default_limits.storage)); try std.testing.expect(!sys.fs.exists(root)); { var writer = try open(allocator, root, smg.default_limits.storage); defer writer.close(); try std.testing.expectError( error.GraphStoreBusy, tryOpenRead(allocator, root, smg.default_limits.storage), ); } var reader = try tryOpenRead(allocator, root, smg.default_limits.storage); defer reader.close();}test "read store returns typed migration and repair requirements" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const migration_root = try testRoot(allocator, 4); defer sys.fs.deleteTree(migration_root) catch {}; { var writer = try open(allocator, migration_root, smg.default_limits.storage); defer writer.close(); var dropped = try writer.database.connection.execute(allocator, "DROP TABLE nodes", .{ .durability = .buffered }); dropped.deinit(allocator); _ = try database.commit(&writer.database); } try std.testing.expectError( error.GraphStoreMigrationRequired, openRead(allocator, migration_root, smg.default_limits.storage), ); const repair_root = try testRoot(allocator, 5); defer sys.fs.deleteTree(repair_root) catch {}; { var writer = try open(allocator, repair_root, smg.default_limits.storage); writer.close(); } const database_path = try std.fs.path.join(allocator, &.{ repair_root, paths.smg_dir_name, paths.database_file_name }); const wal_path = try std.fs.path.join(allocator, &.{ repair_root, paths.smg_dir_name, paths.wal_file_name }); try sys.fs.deleteFile(database_path); try sys.fs.deleteFile(wal_path); try std.testing.expectError( error.GraphStoreRepairRequired, openRead(allocator, repair_root, smg.default_limits.storage), );}fn testRoot(allocator: std.mem.Allocator, offset: i64) ![]const u8 { return try std.fmt.allocPrint(allocator, "/tmp/smg-storage-store-test-{x}", .{@as(u64, @intCast(@max(0, sys.time.realMilliTimestamp() + offset)))});}Complete caller list for storage.store.open
24 direct callers.
tiny.smg.search.rebuildStored[function] attools/smg/src/search.zig:257tools.smg.src.search.replaceAndSyncForTest[function] — private; no exact target attools/smg/src/search.zig:1865in nearest public ownertiny.smg.searchtools.smg.src.search.replaceGraphForTest[function] — private; no exact target attools/smg/src/search.zig:1894in nearest public ownertiny.smg.searchtools.smg.src.search.syncStoredForTest[function] — private; no exact target attools/smg/src/search.zig:1841in nearest public ownertiny.smg.searchtools.smg.src.search.test_stored_search_document_plan_preserves_rowids_and_rendered_fields_without_node_snapshots[function] — test; no exact target attools/smg/src/search.zig:1343in nearest public ownertiny.smg.searchtools.smg.src.storage.context.initStore[function] — private; no exact target attools/smg/src/storage/context.zig:779in nearest public ownertiny.smg.storage.contexttools.smg.src.storage.context.saveGraph[function] — private; no exact target attools/smg/src/storage/context.zig:785in nearest public ownertiny.smg.storage.contexttools.smg.src.storage.context.writeNameProjection[function] — private; no exact target attools/smg/src/storage/context.zig:791in nearest public ownertiny.smg.storage.contexttools.smg.src.storage.graph.initStore[function] — private; no exact target attools/smg/src/storage/graph.zig:877in nearest public ownertiny.smg.storage.graphtools.smg.src.storage.graph.replaceForTest[function] — private; no exact target attools/smg/src/storage/graph.zig:883in nearest public ownertiny.smg.storage.graphtools.smg.src.storage.graph.testHeadHash[function] — private; no exact target attools/smg/src/storage/graph.zig:889in nearest public ownertiny.smg.storage.graphtools.smg.src.storage.graph.test_matched_publication_preserves_the_graph_and_repairs_summary_and_names[function] — test; no exact target attools/smg/src/storage/graph.zig:582in nearest public ownertiny.smg.storage.graphtools.smg.src.storage.nodes.initStore[function] — private; no exact target attools/smg/src/storage/nodes.zig:302in nearest public ownertiny.smg.storage.nodestools.smg.src.storage.nodes.saveGraph[function] — private; no exact target attools/smg/src/storage/nodes.zig:308in nearest public ownertiny.smg.storage.nodestools.smg.src.storage.nodes.test_bounded_node_listing_keeps_lexical_order_independent_of_rowid[function] — test; no exact target attools/smg/src/storage/nodes.zig:260in nearest public ownertiny.smg.storage.nodestiny.smg.storage.project.initProject[function] attools/smg/src/storage/project.zig:13tools.smg.src.storage.store.test_expected_publication_rejects_stale_heads_before_arming_the_refresh_marker[function] — test; no exact target attools/smg/src/storage/store.zig:293in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_read_store_observation_preserves_absence_and_refuses_a_held_writer[function] — test; no exact target attools/smg/src/storage/store.zig:421in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_read_store_opens_after_writer_closes[function] — test; no exact target attools/smg/src/storage/store.zig:253in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_read_store_rejects_an_incomplete_graph_refresh_until_a_writer_completes_it[function] — test; no exact target attools/smg/src/storage/store.zig:266in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_read_store_returns_typed_migration_and_repair_requirements[function] — test; no exact target attools/smg/src/storage/store.zig:442in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_read_store_shared_locks_coexist_and_exclude_a_publisher[function] — test; no exact target attools/smg/src/storage/store.zig:336in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_store_open_creates_directory_gitignore_and_lock[function] — test; no exact target attools/smg/src/storage/store.zig:233in nearest public ownertiny.smg.storage.storetools.smg.src.test.replaceStoredGraph[function] — private; no exact target attools/smg/src/test.zig:26in nearest public ownertools.smg.src.test
Complete caller list for storage.store.openRead
20 direct callers.
tools.smg.src.command.about.command.stored[function] — private; no exact target attools/smg/src/command/about/command.zig:54in nearest public ownertiny.smg.command.about.commandtools.smg.src.command.check.refresh.loadSource[function] — private; no exact target attools/smg/src/command/check/refresh.zig:106in nearest public ownertiny.smg.command.check.refreshtiny.smg.command.session.loadGraphAndConcepts[function] attools/smg/src/command/session.zig:73tiny.smg.concepts.loadSnapshot[function] attools/smg/src/concepts.zig:562tiny.smg.search.searchStored[function] attools/smg/src/search.zig:651tiny.smg.storage.context.load[function] attools/smg/src/storage/context.zig:350tiny.smg.storage.context.loadNodes[function] attools/smg/src/storage/context.zig:377tiny.smg.storage.context.resolveNodeName[function] attools/smg/src/storage/context.zig:34tools.smg.src.storage.context.test_name_projection_preserves_index_fallback_resolutions[function] — test; no exact target attools/smg/src/storage/context.zig:560in nearest public ownertiny.smg.storage.contexttools.smg.src.storage.context.test_resolve_node_name_cleans_every_fallback_allocation_failure[function] — test; no exact target attools/smg/src/storage/context.zig:717in nearest public ownertiny.smg.storage.contexttools.smg.src.storage.graph.loadRecords[function] — private; no exact target attools/smg/src/storage/graph.zig:413in nearest public ownertiny.smg.storage.graphtiny.smg.storage.graph.loadSnapshot[function] attools/smg/src/storage/graph.zig:243tools.smg.src.storage.nodes.listedBounded[function] — private; no exact target attools/smg/src/storage/nodes.zig:86in nearest public ownertiny.smg.storage.nodestiny.smg.storage.nodes.rowsById[function] attools/smg/src/storage/nodes.zig:22tiny.smg.storage.nodes.sortedRows[function] attools/smg/src/storage/nodes.zig:158tools.smg.src.storage.store.test_read_store_observation_preserves_absence_and_refuses_a_held_writer[function] — test; no exact target attools/smg/src/storage/store.zig:421in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_read_store_opens_after_writer_closes[function] — test; no exact target attools/smg/src/storage/store.zig:253in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_read_store_rejects_an_incomplete_graph_refresh_until_a_writer_completes_it[function] — test; no exact target attools/smg/src/storage/store.zig:266in nearest public ownertiny.smg.storage.storetools.smg.src.storage.store.test_read_store_returns_typed_migration_and_repair_requirements[function] — test; no exact target attools/smg/src/storage/store.zig:442in nearest public ownertiny.smg.storage.storetiny.smg.storage.summary.load[function] attools/smg/src/storage/summary.zig:31
Audit
| Definitions | 14 |
|---|---|
| Public names | 14 |
| Members | 10 |
| Version | 26.7.0 |
| Revision | daab053ee433 |