Skip to content

Commit

Permalink
Merge pull request #32 from entangled/feature/16/reduce-conflicting-c…
Browse files Browse the repository at this point in the history
…hanges

Feature/16/reduce conflicting changes
  • Loading branch information
jhidding authored Oct 19, 2023
2 parents ba905a8 + ce7bede commit a7a06ff
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 12 deletions.
4 changes: 3 additions & 1 deletion entangled/commands/stitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def get(item: Content):
@argh.arg("-s", "--show", help="only show, don't act")
def stitch(*, force: bool = False, show: bool = False):
"""Stitch code changes back into the Markdown"""
input_file_list = list(chain.from_iterable(map(Path(".").glob, config.watch_list)))
include_file_list = chain.from_iterable(map(Path(".").glob, config.watch_list))
exclude_file_list = list(chain.from_iterable(map(Path(".").glob, config.ignore_list)))
input_file_list = [path for path in include_file_list if not path in exclude_file_list]

if show:
mode = TransactionMode.SHOW
Expand Down
4 changes: 3 additions & 1 deletion entangled/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ def _stitch_then_tangle():


def sync_action() -> Optional[Callable[[], None]]:
input_file_list = list(chain.from_iterable(map(Path(".").glob, config.watch_list)))
include_file_list = chain.from_iterable(map(Path(".").glob, config.watch_list))
exclude_file_list = list(chain.from_iterable(map(Path(".").glob, config.ignore_list)))
input_file_list = [path for path in include_file_list if not path in exclude_file_list]

with file_db(readonly=True) as db:
changed = set(db.changed())
Expand Down
5 changes: 4 additions & 1 deletion entangled/commands/tangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ def tangle(*, annotate: Optional[str] = None, force: bool = False, show: bool =
else:
annotation_method = AnnotationMethod[annotate.upper()]

input_file_list = chain.from_iterable(map(Path(".").glob, config.watch_list))
include_file_list = chain.from_iterable(map(Path(".").glob, config.watch_list))
exclude_file_list = list(chain.from_iterable(map(Path(".").glob, config.ignore_list)))
input_file_list = [path for path in include_file_list if not path in exclude_file_list]

refs = ReferenceMap()
hooks = get_hooks()

Expand Down
1 change: 1 addition & 0 deletions entangled/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Config(threading.local):
languages: list[Language] = field(default_factory=list)
markers: Markers = field(default_factory=lambda: copy(markers))
watch_list: list[str] = field(default_factory=lambda: ["**/*.md"])
ignore_list: list[str] = field(default_factory=lambda: ["**/README.md"])
annotation_format: Optional[str] = None
annotation: AnnotationMethod = AnnotationMethod.STANDARD
use_line_directives: bool = False
Expand Down
9 changes: 8 additions & 1 deletion entangled/filedb.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ class FileStat:
deps: Optional[list[Path]]
modified: datetime
hexdigest: str
size: int

@staticmethod
def from_path(path: Path, deps: Optional[list[Path]]):
stat = os.stat(path)
size = stat.st_size
with open(path, "r") as f:
digest = hexdigest(f.read())
return FileStat(path, deps, datetime.fromtimestamp(stat.st_mtime), digest)

return FileStat(path, deps, datetime.fromtimestamp(stat.st_mtime), digest, size)

def __lt__(self, other: FileStat) -> bool:
return self.modified < other.modified
Expand All @@ -51,6 +54,7 @@ def from_json(data) -> FileStat:
None if data["deps"] is None else [Path(d) for d in data["deps"]],
datetime.fromisoformat(data["modified"]),
data["hexdigest"],
data["size"]
)

def to_json(self):
Expand All @@ -59,6 +63,7 @@ def to_json(self):
"deps": None if self.deps is None else [str(p) for p in self.deps],
"modified": self.modified.isoformat(),
"hexdigest": self.hexdigest,
"size": self.size
}


Expand Down Expand Up @@ -154,6 +159,8 @@ def initialize() -> FileDB:
"File `%s` in DB doesn't exist. Removing entry from DB.", path
)
del db[path]
if len(undead) > 0:
db.write()
return db

FileDB.path().parent.mkdir(parents=True, exist_ok=True)
Expand Down
4 changes: 3 additions & 1 deletion entangled/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ def find_watch_dirs():

def list_input_files():
"""List all input files."""
return chain.from_iterable(map(Path(".").glob, config.watch_list))
include_file_list = chain.from_iterable(map(Path(".").glob, config.watch_list))
exclude_file_list = list(chain.from_iterable(map(Path(".").glob, config.ignore_list)))
return [path for path in include_file_list if not path in exclude_file_list]


def list_dependent_files():
Expand Down
31 changes: 24 additions & 7 deletions entangled/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from contextlib import contextmanager
from enum import Enum

import os
import tempfile
import logging

try:
Expand All @@ -14,7 +16,7 @@
WITH_RICH = False

from .utility import cat_maybes
from .filedb import FileDB, stat, file_db
from .filedb import FileDB, stat, file_db, hexdigest
from .errors.internal import InternalError


Expand All @@ -41,13 +43,25 @@ class Create(Action):

def conflict(self, _) -> Optional[str]:
if self.target.exists():
return f"{self.target} already exists and is not managed by Entangled"
# Check if file contents are the same as what we want to write or is empty
# then it is safe to take ownership.
md_stat = stat(self.target)
fileHexdigest = md_stat.hexdigest
contentHexdigest = hexdigest(self.content)
if (contentHexdigest == fileHexdigest) or (md_stat.size == 0):
return None
return f"{self.target} is not managed by Entangled"
return None

def run(self, db: FileDB):
self.target.parent.mkdir(parents=True, exist_ok=True)
with open(self.target, "w") as f:
# Write to tmp file then replace with file name
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write(self.content)
# Flush and sync contents to disk
f.flush()
os.fsync(f.fileno())
os.replace(f.name, self.target)
db.update(self.target, self.sources)
if self.sources != []:
db.managed.add(self.target)
Expand All @@ -72,8 +86,13 @@ def conflict(self, db: FileDB) -> Optional[str]:
return None

def run(self, db: FileDB):
with open(self.target, "w") as f:
# Write to tmp file then replace with file name
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write(self.content)
# Flush and sync contents to disk
f.flush()
os.fsync(f.fileno())
os.replace(f.name, self.target)
db.update(self.target, self.sources)

def __str__(self):
Expand All @@ -85,9 +104,7 @@ class Delete(Action):
def conflict(self, db: FileDB) -> Optional[str]:
st = stat(self.target)
if st != db[self.target]:
return (
f"{self.target} seems to have changed outside the control of Entangled"
)
return f"{self.target} seems to have changed outside the control of Entangled"
return None

def run(self, db: FileDB):
Expand Down
63 changes: 63 additions & 0 deletions test/test_ignore_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from entangled.config import config
from entangled.status import find_watch_dirs, list_input_files
from contextlib import chdir
from entangled.commands.tangle import tangle
from pathlib import Path

readme_md = """
# README
```{.python file=src/test.py}
print("test")
```
"""

index_md_1 = """
# Test
``` {.c file=src/test.c}
#include <stdio.h>
int main() { printf("Hello, World!\\n"); return 0; }
```
"""

index_md_2 = """
``` {.makefile file=Makefile}
.RECIPEPREFIX = >
%.o: %.c
> gcc -c $< -o $@
hello: test.o
> gcc $^ -o $@
```
"""

data_md = """
Don't tangle me!
```{.python file=src/test2.py}
print("test2")
```
"""

def list_files_recursive(directory):
for root, _, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
print(file_path)

def test_watch_dirs(tmp_path):
with chdir(tmp_path):
Path("./docs").mkdir()
Path("./docs/data").mkdir()
Path("./docs/index.md").write_text(index_md_1)
Path("./docs/data/data.md").write_text(data_md)
Path("./docs/README.md").write_text(readme_md)
with config(watch_list=["**/*.md"], ignore_list=["**/data/*", "**/README.md"]):
tangle()
# data.md and README.md should not be entangled cause they are part
# of the ignore list while index.md should be so test2.py, test.py
# should not be created while test.c should be created.
assert not Path.exists(Path("./src/test2.py"))
assert not Path.exists(Path("./src/test.py"))
assert Path.exists(Path("./src/test.c"))

0 comments on commit a7a06ff

Please sign in to comment.