s: making diff *

ignore the given paths, including the other path (you get it)
This commit is contained in:
zy 2025-05-04 00:07:20 +02:00
parent 376271f79d
commit 0db6c387bb
46 changed files with 452 additions and 0 deletions

0
a/b Normal file
View file

0
a/c Normal file
View file

1
a/cc Symbolic link
View file

@ -0,0 +1 @@
/home/zy/local/dirf/a/c

0
a/d/b Normal file
View file

1
a/d/bb Symbolic link
View file

@ -0,0 +1 @@
a/b

0
a/d/c Normal file
View file

1
a/d/cc Symbolic link
View file

@ -0,0 +1 @@
/home/zy/local/dirf/a/c

0
a/d/d/b Normal file
View file

1
a/d/d/bb Symbolic link
View file

@ -0,0 +1 @@
a/b

0
a/d/d/c Normal file
View file

1
a/d/d/cc Symbolic link
View file

@ -0,0 +1 @@
/home/zy/local/dirf/a/c

0
a/d/d/d/b Normal file
View file

1
a/d/d/d/bb Symbolic link
View file

@ -0,0 +1 @@
a/b

0
a/d/d/d/c Normal file
View file

1
a/d/d/d/cc Symbolic link
View file

@ -0,0 +1 @@
/home/zy/local/dirf/a/c

1
a/e Normal file
View file

@ -0,0 +1 @@
eyye

1
a/f Normal file
View file

@ -0,0 +1 @@
ayya

1
a/g/e Normal file
View file

@ -0,0 +1 @@
eyye

1
a/g/f Normal file
View file

@ -0,0 +1 @@
ayya

0
b/b Normal file
View file

1
b/bb Symbolic link
View file

@ -0,0 +1 @@
a/b

0
b/c Normal file
View file

0
b/cc Normal file
View file

1
b/d/b Normal file
View file

@ -0,0 +1 @@
eheheh

1
b/d/bb Symbolic link
View file

@ -0,0 +1 @@
a/b

0
b/d/c Normal file
View file

1
b/d/cc Symbolic link
View file

@ -0,0 +1 @@
/home/zy/local/dirf/a/c

0
b/d/d/b Normal file
View file

1
b/d/d/bb Symbolic link
View file

@ -0,0 +1 @@
a/b

0
b/d/d/c Normal file
View file

1
b/d/d/cc Symbolic link
View file

@ -0,0 +1 @@
/home/zy/local/dirf/a/c

0
b/d/d/d/b Normal file
View file

1
b/d/d/d/bb Symbolic link
View file

@ -0,0 +1 @@
a/b

0
b/d/d/d/c Normal file
View file

1
b/d/d/d/cc Symbolic link
View file

@ -0,0 +1 @@
/home/zy/local/dirf/a/c

1
b/e Normal file
View file

@ -0,0 +1 @@
eyye

1
b/f Normal file
View file

@ -0,0 +1 @@
oyyo

1
b/g/e Normal file
View file

@ -0,0 +1 @@
eyye

1
b/g/f Normal file
View file

@ -0,0 +1 @@
ayya

188
dirf/Config.py Normal file
View file

@ -0,0 +1,188 @@
__all__ = [ "Config" ]
from typing import Final, Literal, TextIO, cast
import os.path
from portalocker import Lock
from portalocker.exceptions import AlreadyLocked
from .mytypes import *
class Config:
_file:Final[TextIO]
_source:Final[str]
_dest:Final[str]
_paths_to_ignore:Final[list[PathAndType]]
_paths_to_sync:Final[list[PathAndType]]
_paths_added_to_ignore:list[PathAndType]
_paths_added_to_sync:list[PathAndType]
def __init__(self, filename:str, *,
source:str|None, dest:str|None,
) -> None:
try:
self._file = cast(TextIO,
Lock(filename, "r").acquire(fail_when_locked=True)
)
except AlreadyLocked:
raise Exception(f"the config file '{filename}' is already used.")
lines = self._file.readlines()
self._paths_to_ignore = []
self._paths_to_sync = []
config_source:tuple[int,str]|None = None
config_dest:tuple[int,str]|None = None
current_category:list[PathAndType]|None = None
for i in range(len(lines)):
line = lines[i].rstrip()
head = line.split()[0]
body = line[len(head):].lstrip()
if line == "":
continue
elif line.startswith("#"):
continue
elif head == "SOURCE":
if config_source is not None:
raise Exception(f"line {i+1}: the config already "
+ f"defined SOURCE at line {config_source[0]+1}.")
if os.path.isabs(body):
raise Exception(f"line {i+1}: "
+ "this ain't an absolute path.")
config_source = i, body
elif head == "DEST":
if config_dest is not None:
raise Exception(f"line {i+1}: the config already "
+ f"defined DEST at line {config_dest[0]+1}.")
body = line[4:].lstrip()
if os.path.isabs(body):
raise Exception(f"line {i+1}: "
+ "this ain't an absolute path.")
config_dest = i, body
elif line == "[IGNORE]":
current_category = self._paths_to_ignore
elif line == "[SYNC]":
current_category = self._paths_to_sync
elif head in ["d", "f", "l"]:
head = cast(Literal["d","f","l"], head)
if current_category is None:
raise Exception(f"line {i+1}: "
+ "the path is defined before its category name.")
if os.path.isabs(body):
raise Exception(f"line {i+1}: "
+ "this must be an absolute path.")
if any([
body in [v[1] for v in l]
for l in [self._paths_to_sync, self._paths_to_ignore]
]):
raise Exception(f"line {i+1}: "
+ "this path was already used.")
current_category.append( (head, body) )
else:
raise Exception(f"line {i+1}: what does that mean")
if dest is None and config_dest is None:
raise Exception("'dest' is unknown.")
if (dest is not None and config_dest is not None
and dest != config_dest[1]
):
raise Exception("the 'dest' given to Config() is different "
+ "than in the config file.")
self._dest = config_dest[1] if config_dest is not None else (
cast(str, dest)
)
if source is None and config_source is None:
raise Exception("'source' is unknown.")
if (source is not None and config_source is not None
and source != config_source[1]
):
raise Exception("the 'source' given to Config() is different "
+ "than in the config file.")
self._source = config_source[1] if config_source is not None else (
cast(str, source)
)
self._paths_added_to_ignore = []
self._paths_added_to_sync = []
def toIgnore(self,
path:str, pathtype:PathType
) -> bool|Literal["different_types"]:
if path in [
path[1] for path in self._paths_to_ignore
if pathtype != path[0]
]:
return "different_types"
return path in [ path[1] for path in self._paths_to_ignore ]
def toSync(self,
path:str, pathtype:PathType
) -> bool|Literal["different_types"]:
if path in [
path[1] for path in self._paths_to_sync
if pathtype != path[0]
]:
return "different_types"
return path in [ path[1] for path in self._paths_to_sync ]
def flush(self) -> None:
if not self._file.writable():
return
raise NotImplemented
def addToIgnore(self,
path:str, pathtype:PathType
) -> bool|Literal["different_types"]:
if not self._file.writable():
raise Exception("the file is read-only.")
if not self.toIgnore(path, pathtype) is False:
if self.toIgnore(path, pathtype) == "different_types":
return "different_types"
return False
self._paths_to_ignore.append( (pathtype, path) )
self._paths_added_to_ignore.append( (pathtype, path) )
return True
def addToSync(self,
path:str, pathtype:PathType
) -> bool|Literal["different_types"]:
if not self._file.writable():
raise Exception("the file is read-only.")
if not self.toSync(path, pathtype) is False:
if self.toSync(path, pathtype) == "different_types":
return "different_types"
return False
self._paths_to_sync.append( (pathtype, path) )
self._paths_added_to_sync.append( (pathtype, path) )
return True
def __enter__(self) -> "Config":
return self
def __exit__(self, *_) -> None:
self.flush()
def __del__(self) -> None:
self.flush()
def getSource(self) -> str:
return self._source
def getDest(self) -> str:
return self._dest

0
dirf/__init__.py Normal file
View file

9
dirf/__main__.py Normal file
View file

@ -0,0 +1,9 @@
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("")
if __name__ == "__main__":
main()

162
dirf/diff.py Normal file
View file

@ -0,0 +1,162 @@
__add__ = [ "diff" ]
from collections.abc import Set
from os import listdir, readlink
from os.path import abspath
from typing import Callable, Final
from collections.abc import Iterable
from .mytypes import *
from .Config import *
from .utils import *
from .whatsthispath import *
from .utils import *
class DiffResult:
def __init__(self, path:str) -> None:
self.path:Final[str] = path
self.differs:set[PathAndType] = set()
self.same:set[PathAndType] = set()
self.differentTypes:set[tuple[str,PathType,PathType]] = set()
self.presentOnlyInFirst:set[PathAndType] = set()
self.presentOnlyInSecond:set[PathAndType] = set()
self.weird:set[str] = set()
self.subresults:dict[str,DiffResult] = {}
def simplify(self, *, deep:bool) -> None:
for l in [
self.differs, self.same, self.presentOnlyInFirst,
self.presentOnlyInSecond
]:
mapSet(l, ( lambda v: ( v[0], simplifyPath(v[1]) ) ))
mapSet(self.differentTypes, (
lambda v: ( simplifyPath(v[0]), *v[1:] )
))
mapSet(self.weird, simplifyPath)
if deep:
for v in self.subresults.values():
v.simplify(deep=True)
def print(self, source:str|None = None, target:str|None = None) -> None:
reprSource = repr(source) if source is not None else "the source directory"
reprTarget = repr(target) if target is not None else "the target directory"
def reprPathAndType(v:PathAndType) -> str:
return f"{lineForPathAndType(v)}"
def printWith[T](label:str, what:set[T], f:Callable[[T],str]):
if len(what) == 0:
return
print(label + ":")
for v in what:
print(f"-\t{f(v)}")
title(f"for path {repr(self.path)}")
printWith("same", self.same, reprPathAndType)
printWith("different", self.differs, reprPathAndType)
printWith("different types", self.differentTypes,
lambda v: f"{reprPathType(v[1])} / {reprPathType(v[2])}: {v[0]}"
)
printWith(f"exist only in {reprSource}",
self.presentOnlyInFirst, reprPathAndType
)
printWith(f"exist only in {reprTarget}",
self.presentOnlyInSecond, reprPathAndType
)
printWith("dunno what that is lol", self.weird,
lambda v: v
)
def diff(pathA:str, pathB:str, *, ignored:Set[PathAndType]) -> DiffResult:
r = _diffDirs(abspath(pathA), abspath(pathB), ignored, "/", trust=False)
return r
def _diffDirs(
absPathA:str, absPathB:str, absIgnoredPaths:Set[PathAndType],
relCurrentPath:str, *, trust:bool
) -> DiffResult:
absCurrentPathA = absPathA + "/" + relCurrentPath
absCurrentPathB = absPathB + "/" + relCurrentPath
absCurrentPathA_what = whatsthispath(absCurrentPathA)
absCurrentPathB_what = whatsthispath(absCurrentPathB)
if not trust:
assert absPathA[0] == "/", "'pathA' should be an absolute path."
assert absPathB[0] == "/", "'pathB' should be an absolute path."
assert relCurrentPath[0] == "/", (
"'relCurrentPath' should start with '/'."
)
assert all([ v[1][0] == "/" for v in absIgnoredPaths ]), (
"'ignoredPaths' must contain only absolute paths."
)
assert absCurrentPathA_what == 'd', "'pathA' should be a directory."
assert absCurrentPathB_what == 'd', "'pathB' should be a directory."
r = DiffResult(relCurrentPath)
for name in set(listdir(absCurrentPathA)) | set(listdir(absCurrentPathB)):
relNewPath = relCurrentPath + "/" + name
absNewPathA = absCurrentPathA + "/" + name
absNewPathB = absCurrentPathB + "/" + name
absNewPathA_what = whatsthispath(absNewPathA)
absNewPathB_what = whatsthispath(absNewPathB)
if absNewPathA_what is None:
assert absNewPathB_what is not None
r.presentOnlyInSecond.add((absNewPathB_what, relNewPath))
elif absNewPathB_what is None:
r.presentOnlyInFirst.add((absNewPathA_what, relNewPath))
elif absNewPathA_what != absNewPathB_what:
r.differentTypes.add(
(relNewPath, absNewPathA_what, absNewPathB_what)
)
else:
_diffOne(r,
absPathA, absPathB, absIgnoredPaths, relNewPath, name,
absNewPathA_what
)
r.simplify(deep=False)
return r
def _diffOne(r:DiffResult, absPathA:str, absPathB:str, absIgnoredPaths:Set[PathAndType],
relCurrentPath:str, filename:str, relCurrentPath_what:PathType
) -> None:
match relCurrentPath_what:
case "d":
rr = _diffDirs(absPathA, absPathB, absIgnoredPaths, relCurrentPath, trust=False) # TODO(debug) add trust=True
r.subresults[filename] = rr
if (len(rr.differentTypes) > 0
or len(rr.presentOnlyInFirst) > 0
or len(rr.presentOnlyInSecond) > 0
or len(rr.differs) > 0
):
r.differs.add( (relCurrentPath_what, filename) )
elif len(rr.weird) > 0:
r.weird.add( filename )
else:
r.same.add( ( relCurrentPath_what, filename ) )
case "f"|"l":
getData:Callable[[str],str|bytes] = (
(lambda path: open(path, "rb").read())
if relCurrentPath_what == "f" else
(lambda path: readlink(path))
)
(
r.same if (
getData(absPathA + "/" + relCurrentPath)
== getData(absPathB + "/" + relCurrentPath)
) else r.differs
).add( (relCurrentPath_what, filename) )
case "w":
r.weird.add( filename )

28
dirf/mytypes.py Normal file
View file

@ -0,0 +1,28 @@
__all__ = [
"PathType",
"PathAndType",
"reprPathType",
"lineForPathAndType",
]
from typing import Literal
type PathType = Literal["d","f","l","w"]
def reprPathType(t:PathType) -> str:
match t:
case "d":
return "directory"
case "f":
return "file"
case "l":
return "symbolic link"
case "w":
return "unknown type"
type PathAndType = tuple["PathType",str]
def lineForPathAndType(v:PathAndType) -> str:
return f"{reprPathType(v[0])}: {v[1]}"

25
dirf/utils.py Normal file
View file

@ -0,0 +1,25 @@
__all__ = [
"title",
"simplifyPath",
"mapSet",
]
from typing import Callable
def title(txt:str) -> None:
return print(f"\033[1m{txt}\033[0m")
def simplifyPath(path:str) -> str:
start = "/" if path[0] == "/" else ""
return start + "/".join(
[ el for el in path.split("/") if el != "" ]
)
def mapSet[T](s:set[T], f:Callable[[T],T]) -> None:
s2:set[T] = set()
while len(s) > 0:
s2.add(f(s.pop()))
s.update(s2)

17
dirf/whatsthispath.py Normal file
View file

@ -0,0 +1,17 @@
__all__ = [ "whatsthispath" ]
import os.path
from .mytypes import *
def whatsthispath(path:str) -> PathType|None:
if os.path.islink(path):
return "l"
if os.path.isdir(path):
return "d"
if os.path.isfile(path):
return "f"
if os.path.exists(path):
return "w"
return None