shiggles 0.2.0__py3-none-any.whl

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.
shiggles/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Shiggles - for git and shiggles. A simple content-addressed VCS."""
2
+
3
+ __version__ = "0.2.0"
shiggles/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from shiggles.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
shiggles/cli.py ADDED
@@ -0,0 +1,67 @@
1
+ """CLI entry point for shig."""
2
+
3
+ import sys
4
+
5
+ from shiggles import __version__
6
+ from shiggles.commands import (
7
+ cmd_bring,
8
+ cmd_changes,
9
+ cmd_download,
10
+ cmd_history,
11
+ cmd_load,
12
+ cmd_save,
13
+ cmd_start,
14
+ cmd_tab,
15
+ cmd_timeline,
16
+ cmd_undo,
17
+ cmd_untab,
18
+ cmd_upload,
19
+ )
20
+ from shiggles.util import die
21
+
22
+ COMMANDS = {
23
+ "start": cmd_start,
24
+ "save": cmd_save,
25
+ "load": cmd_load,
26
+ "changes": cmd_changes,
27
+ "history": cmd_history,
28
+ "undo": cmd_undo,
29
+ "tab": cmd_tab,
30
+ "untab": cmd_untab,
31
+ "bring": cmd_bring,
32
+ "download": cmd_download,
33
+ "upload": cmd_upload,
34
+ "timeline": cmd_timeline,
35
+ }
36
+
37
+ HELP = f"""Shiggles v{__version__} - for git and shiggles.
38
+
39
+ Commands:
40
+ shig start set up this folder
41
+ shig save "message" snapshot current tab
42
+ shig save --yes "message" save even if remote is ahead (skip nudge)
43
+ shig load ["message"] put files back to a save (becomes current)
44
+ shig changes what's different since last save
45
+ shig history saves on current tab
46
+ shig tab [name] list tabs, or switch / create
47
+ shig untab <name> remove a tab (history kept)
48
+ shig bring <tab> land current tab into another (usually main)
49
+ shig download [path] get latest current tab (folder or URL)
50
+ shig upload [path] send current tab (folder or URL)
51
+ shig undo discard unsaved changes, restore last save
52
+ shig timeline operation log
53
+ """
54
+
55
+
56
+ def main():
57
+ if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
58
+ print(HELP)
59
+ return
60
+ cmd = sys.argv[1]
61
+ if cmd not in COMMANDS:
62
+ die(f"Unknown command: {cmd}\n -> shig help")
63
+ COMMANDS[cmd](sys.argv[2:])
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
shiggles/commands.py ADDED
@@ -0,0 +1,414 @@
1
+ """All shig commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from shiggles.conflicts import (
9
+ find_conflicts,
10
+ format_conflict_message,
11
+ merge_indices,
12
+ park_files,
13
+ )
14
+ from shiggles.ignore import ensure_shignore
15
+ from shiggles.paths import DEFAULT_TAB, SHIG
16
+ from shiggles.repo import (
17
+ current_tab,
18
+ get_tab_commit,
19
+ init_repo,
20
+ list_saves_for_tab,
21
+ list_tabs,
22
+ log_event,
23
+ merge_timeline_lines,
24
+ merged_timeline_for_remote,
25
+ read_timeline_lines,
26
+ require_repo,
27
+ set_current_tab,
28
+ set_tab_commit,
29
+ tab_fork,
30
+ tab_ref,
31
+ )
32
+ from shiggles.store import parse_commit, write_commit
33
+ from shiggles.remote import LocalRemote, Remote, get_remote, is_ancestor, resolve_remote_arg
34
+ from shiggles.trees import (
35
+ build_tree_index,
36
+ commit_preview,
37
+ load_tree,
38
+ working_tree_index,
39
+ write_tree,
40
+ write_tree_from_index,
41
+ )
42
+ from shiggles.util import author_name, die, human_ago, load_config, save_config
43
+
44
+ ROOT = Path(".")
45
+
46
+
47
+ def _fork_index(tab: str) -> dict[str, str]:
48
+ fork = tab_fork(tab)
49
+ if not fork.exists():
50
+ return {}
51
+ commit = fork.read_text(encoding="utf-8").strip()
52
+ if not commit:
53
+ return {}
54
+ info = parse_commit(commit)
55
+ return build_tree_index(info["tree"])
56
+
57
+
58
+ def _maybe_autosave(message: str):
59
+ head = get_tab_commit()
60
+ current = working_tree_index(ROOT)
61
+ previous = build_tree_index(parse_commit(head)["tree"]) if head else {}
62
+ if current == previous:
63
+ return
64
+ _do_save(message, quiet=True)
65
+
66
+
67
+ def _do_save(message: str, *, quiet: bool = False):
68
+ tree = write_tree(ROOT)
69
+ tab = current_tab()
70
+ parent = get_tab_commit(tab)
71
+
72
+ if parent:
73
+ if parse_commit(parent)["tree"] == tree:
74
+ die("Nothing changed since last save.\n -> shig changes")
75
+
76
+ commit_hash = write_commit(tree, parent, message, author_name(), int(time.time()))
77
+ set_tab_commit(tab, commit_hash)
78
+ log_event(f"save [{tab}] {commit_hash[:10]} {message!r}")
79
+ if not quiet:
80
+ print(f'Saved on {tab}: "{message}"')
81
+
82
+
83
+ def cmd_start(_args):
84
+ if SHIG.exists():
85
+ die("Already a Shiggles repo here.\n -> nothing to do.")
86
+ init_repo()
87
+ ensure_shignore()
88
+ log_event("start")
89
+ print("Watching this folder.")
90
+ print(' -> shig save "your first version"')
91
+
92
+
93
+ def _maybe_nudge_download(*, force: bool):
94
+ cfg = load_config()
95
+ if "remote" not in cfg:
96
+ return
97
+ try:
98
+ remote = get_remote(cfg["remote"])
99
+ except SystemExit:
100
+ return
101
+ tab = current_tab()
102
+ local_commit = get_tab_commit(tab)
103
+ remote_commit = remote.read_tab_commit(tab)
104
+ if not remote_commit or remote_commit == local_commit:
105
+ return
106
+ if local_commit:
107
+ remote.sync_objects_from(remote_commit)
108
+ if is_ancestor(local_commit, remote_commit):
109
+ print(f"Someone else saved to {tab} since your last download.")
110
+ print(" -> shig download first")
111
+ if force:
112
+ return
113
+ try:
114
+ answer = input("Save anyway? [y/N] ").strip().lower()
115
+ except EOFError:
116
+ die("Cancelled.")
117
+ if answer != "y":
118
+ die("Cancelled. Run shig download first.")
119
+
120
+
121
+ def cmd_save(args):
122
+ require_repo()
123
+ force = False
124
+ if args and args[0] == "--yes":
125
+ force = True
126
+ args = args[1:]
127
+ if not args:
128
+ die('Save needs a message.\n -> shig save "what you did"')
129
+ _maybe_nudge_download(force=force)
130
+ _do_save(args[0])
131
+
132
+
133
+ def cmd_load(args):
134
+ require_repo()
135
+ tab = current_tab()
136
+ head = get_tab_commit()
137
+ saves = list_saves_for_tab(tab)
138
+ if not saves:
139
+ die('No saves yet.\n -> shig save "first version"')
140
+
141
+ target = None
142
+ if args:
143
+ needle = args[0].lower()
144
+ for h, msg in saves:
145
+ if needle in msg.lower() or h.startswith(needle):
146
+ target = h
147
+ break
148
+ if not target:
149
+ die(f'No save matching "{args[0]}".\n -> shig load')
150
+ else:
151
+ print("Pick a save to load:")
152
+ for i, (ch, msg) in enumerate(saves, 1):
153
+ preview = commit_preview(ch)
154
+ print(f" {i}) {msg} - {preview}")
155
+ try:
156
+ choice = input("> ").strip()
157
+ idx = int(choice) - 1
158
+ target = saves[idx][0]
159
+ except (ValueError, IndexError, EOFError):
160
+ die("Pick a number from the list.")
161
+
162
+ info = parse_commit(target)
163
+ load_tree(info["tree"], ROOT)
164
+ set_tab_commit(tab, target)
165
+ log_event(f"load {target[:10]}")
166
+ print(f'Loaded: "{info["message"]}"')
167
+
168
+
169
+ def cmd_changes(_args):
170
+ require_repo()
171
+ head = get_tab_commit()
172
+ current = working_tree_index(ROOT)
173
+ previous = build_tree_index(parse_commit(head)["tree"]) if head else {}
174
+
175
+ added = [p for p in current if p not in previous]
176
+ removed = [p for p in previous if p not in current]
177
+ changed = [p for p in current if p in previous and current[p] != previous[p]]
178
+
179
+ tab = current_tab()
180
+ if not (added or removed or changed):
181
+ print(f"Clean on {tab}. Nothing changed since last save.")
182
+ return
183
+ print(f"Changes on {tab}:")
184
+ for p in added:
185
+ print(f" + {p}")
186
+ for p in changed:
187
+ print(f" ~ {p}")
188
+ for p in removed:
189
+ print(f" - {p}")
190
+ print('\n -> shig save "..." to snapshot these')
191
+
192
+
193
+ def cmd_history(_args):
194
+ require_repo()
195
+ tab = current_tab()
196
+ saves = list_saves_for_tab(tab)
197
+ if not saves:
198
+ print(f"No saves on {tab} yet.\n -> shig save \"first version\"")
199
+ return
200
+ head = get_tab_commit(tab)
201
+ print(f"History on {tab}:")
202
+ for i, (h, msg) in enumerate(saves):
203
+ info = parse_commit(h)
204
+ when = human_ago(info["time"]) if info["time"] else "?"
205
+ who = info["author"] or "?"
206
+ mark = " *" if h == head else ""
207
+ print(f"* {msg}{mark} ({who}, {when})")
208
+ if i < len(saves) - 1:
209
+ print("|")
210
+
211
+
212
+ def cmd_undo(_args):
213
+ require_repo()
214
+ tab = current_tab()
215
+ head = get_tab_commit(tab)
216
+ if not head:
217
+ die('Nothing to undo.\n -> shig save "first version"')
218
+
219
+ info = parse_commit(head)
220
+ current = working_tree_index(ROOT)
221
+ saved = build_tree_index(info["tree"])
222
+
223
+ if current == saved:
224
+ print(f"Clean on {tab}. Already matches last save.")
225
+ return
226
+
227
+ load_tree(info["tree"], ROOT)
228
+ log_event(f"undo [{tab}]")
229
+ print(f'Undone. Restored last save: "{info["message"]}"')
230
+
231
+
232
+ def cmd_tab(args):
233
+ require_repo()
234
+ tabs = list_tabs()
235
+ if not args:
236
+ cur = current_tab()
237
+ print("Tabs:")
238
+ for t in tabs:
239
+ mark = " *" if t == cur else ""
240
+ head = get_tab_commit(t)
241
+ note = " (no saves yet)" if not head else ""
242
+ print(f" {t}{mark}{note}")
243
+ print("\n -> shig tab <name> to switch or create")
244
+ return
245
+
246
+ name = args[0]
247
+ if name == DEFAULT_TAB and name not in tabs:
248
+ tabs.append(name)
249
+
250
+ cur = current_tab()
251
+ if name != cur:
252
+ _maybe_autosave(f"before switching to {name}")
253
+
254
+ if name not in tabs:
255
+ from shiggles.paths import REFS_TABS
256
+
257
+ fork = get_tab_commit(cur) or ""
258
+ REFS_TABS.mkdir(parents=True, exist_ok=True)
259
+ if fork:
260
+ set_tab_commit(name, fork)
261
+ tab_fork(name).parent.mkdir(parents=True, exist_ok=True)
262
+ tab_fork(name).write_text(fork + "\n", encoding="utf-8")
263
+ else:
264
+ tab_ref(name).touch()
265
+ log_event(f"tab new {name} from {cur}")
266
+
267
+ set_current_tab(name)
268
+ head = get_tab_commit(name)
269
+ if head:
270
+ info = parse_commit(head)
271
+ load_tree(info["tree"], ROOT)
272
+
273
+ print(f"On tab: {name}")
274
+
275
+
276
+ def cmd_untab(args):
277
+ require_repo()
278
+ if not args:
279
+ die("Which tab?\n -> shig untab <name>")
280
+ name = args[0]
281
+ if name == DEFAULT_TAB:
282
+ die("Can't remove the main tab.")
283
+ if name not in list_tabs():
284
+ die(f"No tab named {name!r}.")
285
+ if name == current_tab():
286
+ die(f"Switch off {name} first.\n -> shig tab main")
287
+
288
+ tab_ref(name).unlink(missing_ok=True)
289
+ fork = tab_fork(name)
290
+ if fork.exists():
291
+ fork.unlink()
292
+ log_event(f"untab {name}")
293
+ print(f"Removed tab: {name}")
294
+ print(" (saves still in .shig/ - nothing deleted)")
295
+
296
+
297
+ def cmd_bring(args):
298
+ require_repo()
299
+ if not args:
300
+ die("Bring into which tab?\n -> shig bring main")
301
+ target = args[0]
302
+ source = current_tab()
303
+ if source == target:
304
+ die(f"Already on {target}.\n -> shig tab <name> first")
305
+
306
+ if target not in list_tabs():
307
+ die(f"No tab named {target!r}.\n -> shig tab {target}")
308
+
309
+ ours_commit = get_tab_commit(source)
310
+ theirs_commit = get_tab_commit(target)
311
+ if not ours_commit:
312
+ die(f"Nothing on {source} to bring.\n -> shig save first")
313
+
314
+ ours = build_tree_index(parse_commit(ours_commit)["tree"])
315
+ theirs = build_tree_index(parse_commit(theirs_commit)["tree"]) if theirs_commit else {}
316
+ base = _fork_index(source) if source != DEFAULT_TAB else theirs
317
+
318
+ conflicts = find_conflicts(ours, theirs)
319
+ if conflicts:
320
+ park_files(theirs, conflicts, target)
321
+ die(format_conflict_message("bring", target, source, conflicts, target))
322
+
323
+ merged = merge_indices(base, ours, theirs)
324
+ merged_tree = write_tree_from_index(merged)
325
+ parent = theirs_commit
326
+ msg = f"brought {source} into {target}"
327
+ commit_hash = write_commit(merged_tree, parent, msg, author_name(), int(time.time()))
328
+ set_tab_commit(target, commit_hash)
329
+ load_tree(merged_tree, ROOT)
330
+ set_current_tab(target)
331
+ log_event(f"bring {source} -> {target} {commit_hash[:10]}")
332
+ print(f"Brought {source} -> {target}.")
333
+ print(f" Now on {target}.")
334
+ print(" -> shig upload")
335
+
336
+
337
+ def _apply_download(tab: str, local_commit: str | None, remote_commit: str, remote: Remote):
338
+ remote.sync_objects_from(remote_commit)
339
+ remote.copy_tab_meta_from(tab)
340
+ merge_timeline_lines(remote.read_timeline_lines())
341
+
342
+ if remote_commit == local_commit:
343
+ print(f"{tab} is up to date.")
344
+ return
345
+
346
+ if local_commit and is_ancestor(local_commit, remote_commit):
347
+ set_tab_commit(tab, remote_commit)
348
+ load_tree(parse_commit(remote_commit)["tree"], ROOT)
349
+ log_event(f"download [{tab}] {remote_commit[:10]}")
350
+ print(f"Downloaded latest {tab}.")
351
+ return
352
+
353
+ remote_index = build_tree_index(parse_commit(remote_commit)["tree"])
354
+ local_index = build_tree_index(parse_commit(local_commit)["tree"]) if local_commit else {}
355
+
356
+ conflicts = find_conflicts(local_index, remote_index)
357
+ if conflicts:
358
+ park_files(remote_index, conflicts, "remote")
359
+ die(format_conflict_message("download", tab, "remote", conflicts, "remote"))
360
+
361
+ set_tab_commit(tab, remote_commit)
362
+ load_tree(parse_commit(remote_commit)["tree"], ROOT)
363
+ log_event(f"download [{tab}] {remote_commit[:10]}")
364
+ print(f"Downloaded latest {tab}.")
365
+
366
+
367
+ def cmd_download(args):
368
+ require_repo()
369
+ remote, _ = resolve_remote_arg(args)
370
+ if isinstance(remote, LocalRemote) and not Path(remote.path).exists():
371
+ die(f"Can't find: {remote.path}")
372
+
373
+ tab = current_tab()
374
+ remote_commit = remote.read_tab_commit(tab)
375
+ local_commit = get_tab_commit(tab)
376
+
377
+ if not remote_commit:
378
+ print(f"No {tab} on remote yet. Nothing to download.")
379
+ return
380
+
381
+ _apply_download(tab, local_commit, remote_commit, remote)
382
+
383
+
384
+ def cmd_upload(args):
385
+ require_repo()
386
+ remote, _ = resolve_remote_arg(args)
387
+ remote.ensure_upload_ready()
388
+
389
+ tab = current_tab()
390
+ local_commit = get_tab_commit(tab)
391
+ if not local_commit:
392
+ die("Nothing to upload.\n -> shig save first")
393
+
394
+ remote.sync_objects_to(local_commit)
395
+ remote.copy_tab_meta_to(tab)
396
+ remote.write_tab_commit(tab, local_commit)
397
+ log_event(f"upload [{tab}] {local_commit[:10]}")
398
+ local_lines = read_timeline_lines()
399
+ remote.write_timeline_lines(
400
+ merged_timeline_for_remote(local_lines, remote.read_timeline_lines())
401
+ )
402
+ print(f"Uploaded {tab} to {remote.label()}.")
403
+
404
+
405
+ def cmd_timeline(_args):
406
+ require_repo()
407
+ from shiggles.paths import TIMELINE
408
+
409
+ if not TIMELINE.exists() or TIMELINE.stat().st_size == 0:
410
+ print("Timeline empty.")
411
+ return
412
+ for line in TIMELINE.read_text(encoding="utf-8").splitlines():
413
+ ts, text = line.split("\t", 1)
414
+ print(f" {human_ago(int(ts)):>10} {text}")
shiggles/conflicts.py ADDED
@@ -0,0 +1,98 @@
1
+ import shutil
2
+ from pathlib import Path
3
+
4
+ from shiggles.paths import COLLISIONS
5
+ from shiggles.store import read_object
6
+ from shiggles.trees import build_tree_index
7
+
8
+
9
+ def find_conflicts(ours: dict[str, str], theirs: dict[str, str]) -> list[str]:
10
+ """Paths where both sides have different blob hashes."""
11
+ conflicts = []
12
+ for path in sorted(set(ours) & set(theirs)):
13
+ if ours[path] != theirs[path]:
14
+ conflicts.append(path)
15
+ return conflicts
16
+
17
+
18
+ def park_files(index: dict[str, str], paths: list[str], source: str):
19
+ """Write copies of *theirs* into .shig/collisions/<source>/."""
20
+ dest_root = COLLISIONS / source
21
+ for path in paths:
22
+ bh = index[path]
23
+ _, data = read_object(bh)
24
+ dest = dest_root / path
25
+ dest.parent.mkdir(parents=True, exist_ok=True)
26
+ dest.write_bytes(data)
27
+
28
+
29
+ def clear_collision_dir(source: str):
30
+ root = COLLISIONS / source
31
+ if root.exists():
32
+ shutil.rmtree(root)
33
+
34
+
35
+ def merge_indices(
36
+ base: dict[str, str],
37
+ ours: dict[str, str],
38
+ theirs: dict[str, str],
39
+ ) -> dict[str, str]:
40
+ """
41
+ Merge ours (current tab) into theirs (main/remote).
42
+ - Non-overlapping changes from ours apply.
43
+ - theirs wins for unchanged paths.
44
+ - Deletions from ours (in base, not in ours) apply.
45
+ """
46
+ merged = dict(theirs)
47
+ for path, bh in ours.items():
48
+ if path not in theirs:
49
+ merged[path] = bh
50
+ elif ours[path] == theirs[path]:
51
+ continue
52
+ else:
53
+ # conflict - caller must abort before calling merge
54
+ merged[path] = bh
55
+ for path in base:
56
+ if path in ours:
57
+ continue
58
+ if path in merged:
59
+ del merged[path]
60
+ return merged
61
+
62
+
63
+ def format_conflict_message(
64
+ action: str,
65
+ target: str,
66
+ source: str,
67
+ conflicts: list[str],
68
+ collision_source: str,
69
+ ) -> str:
70
+ lines = [
71
+ f"Can't {action} {source} -> {target}.",
72
+ "",
73
+ " Changed on BOTH sides:",
74
+ ]
75
+ for p in conflicts:
76
+ lines.append(f" {p}")
77
+ lines.append("")
78
+ lines.append(f" {target.capitalize()}'s copies (for comparison):")
79
+ for p in conflicts:
80
+ lines.append(f" .shig/collisions/{collision_source}/{p}")
81
+ lines.append("")
82
+ lines.append(" Your files are unchanged in the project folder.")
83
+ lines.append("")
84
+ lines.append(" Next steps:")
85
+ if action == "bring":
86
+ lines.append(" 1) Open the collision copies and your files side by side")
87
+ if source != target:
88
+ lines.append(f" 2) shig tab {target}")
89
+ lines.append(" 3) shig download (if using a shared folder)")
90
+ lines.append(' 4) Fix the files, then shig save "..."')
91
+ lines.append(" 5) shig upload")
92
+ elif action == "download":
93
+ lines.append(" 1) Open .shig/collisions/remote/ files and your copies")
94
+ lines.append(' 2) Fix the files, then shig save "..."')
95
+ lines.append(" 3) shig download")
96
+ else:
97
+ lines.append(" Compare, fix, save, then try again.")
98
+ return "\n".join(lines)
shiggles/ignore.py ADDED
@@ -0,0 +1,52 @@
1
+ import fnmatch
2
+ import os
3
+ from pathlib import Path, PurePosixPath
4
+
5
+ from shiggles.paths import BUILTIN_IGNORE, DEFAULT_SHIGNORE, SHIGNORE
6
+
7
+
8
+ def ensure_shignore():
9
+ if not SHIGNORE.exists():
10
+ SHIGNORE.write_text(DEFAULT_SHIGNORE, encoding="utf-8")
11
+
12
+
13
+ def _read_patterns() -> list[str]:
14
+ if not SHIGNORE.exists():
15
+ return []
16
+ patterns = []
17
+ for line in SHIGNORE.read_text(encoding="utf-8").splitlines():
18
+ line = line.strip()
19
+ if not line or line.startswith("#"):
20
+ continue
21
+ patterns.append(line)
22
+ return patterns
23
+
24
+
25
+ def _rel_path(path: Path, root: Path) -> str | None:
26
+ rel = os.path.relpath(path.resolve(), root.resolve())
27
+ if rel.startswith(".."):
28
+ return None
29
+ return Path(rel).as_posix()
30
+
31
+
32
+ def _matches(rel: str, patterns: list[str]) -> bool:
33
+ name = PurePosixPath(rel).name
34
+ for pat in patterns:
35
+ if pat.endswith("/"):
36
+ dirpat = pat.rstrip("/")
37
+ if rel == dirpat or rel.startswith(dirpat + "/"):
38
+ return True
39
+ if dirpat in PurePosixPath(rel).parts:
40
+ return True
41
+ elif fnmatch.fnmatch(rel, pat) or fnmatch.fnmatch(name, pat):
42
+ return True
43
+ return False
44
+
45
+
46
+ def ignored(path: Path, root: Path) -> bool:
47
+ if path.name in BUILTIN_IGNORE:
48
+ return True
49
+ rel = _rel_path(path, root)
50
+ if rel is None:
51
+ return True
52
+ return _matches(rel, _read_patterns())
shiggles/paths.py ADDED
@@ -0,0 +1,31 @@
1
+ from pathlib import Path
2
+
3
+ SHIG = Path(".shig")
4
+ OBJECTS = SHIG / "objects"
5
+ REFS_TABS = SHIG / "refs" / "tabs"
6
+ TAB_META = SHIG / "tabs"
7
+ HEAD = SHIG / "HEAD"
8
+ TIMELINE = SHIG / "timeline.log"
9
+ CONFIG = SHIG / "config.json"
10
+ COLLISIONS = SHIG / "collisions"
11
+ SHIGNORE = Path(".shignore")
12
+
13
+ DEFAULT_TAB = "main"
14
+
15
+ BUILTIN_IGNORE = {".shig", ".git", "__pycache__", ".idea", ".vscode", "node_modules"}
16
+
17
+ DEFAULT_SHIGNORE = """\
18
+ # Shiggles - files and folders we never snapshot
19
+ .shig/
20
+ .git/
21
+ __pycache__/
22
+ *.pyc
23
+ .DS_Store
24
+ Thumbs.db
25
+ release/
26
+ build/
27
+ dist/
28
+ bin/
29
+ obj/
30
+ node_modules/
31
+ """