diff --git a/sos/sos.coco b/sos/sos.coco index 9cdaf49..502f7fa 100644 --- a/sos/sos.coco +++ b/sos/sos.coco @@ -65,7 +65,7 @@ Usage: {cmd} [] [, ...] When operating in of --soft Don't move or rename files, only the tracking pattern rm [] Remove a tracking pattern. Only useful after "offline --track" or "offline --picky" - ls List file tree and mark changes and tracking status + ls [] [--patterns] List file tree and mark changes and tracking status status List branches and display repository status log [--changes] List commits of current branch config [set/unset/show/add/rm] [ []] Configure user-global defaults. @@ -550,6 +550,9 @@ def status() -> None: m.loadBranches() # knows current branch current:int = m.branch info("//|\\\\ Offline repository status") + print("Content checking %sactivated" % ("" if m.strict else "de")) + print("Data compression %sactivated" % ("" if m.compress else "de")) + print("Repository is in %s mode" % ("tracking" if m.track else ("picky" if m.picky else "simple"))) sl:int = max([len(b.name ?? "") for b in m.branches.values()]) for branch in sorted(m.branches.values(), key = (b) -> b.number): m.loadBranch(branch.number) # knows commit history @@ -712,26 +715,31 @@ def remove(relPath:str, pattern:str) -> None: m.saveBranches() info("Removed tracking pattern '%s' for folder '%s'" % (os.path.basename(pattern), os.path.abspath(relPath.replace(SLASH, os.sep)))) -def ls(argument:str? = None) -> None: +def ls(argument:str? = None, options:str[] = []) -> None: ''' List specified directory, augmenting with repository metadata. ''' cwd:str = os.getcwd() if argument is None else argument m:Metadata = Metadata(cwd) m.loadBranches() + info("Repository is in %s mode" % ("tracking" if m.track else ("picky" if m.picky else "simple"))) relPath:str = os.path.relpath(cwd, m.root).replace(os.sep, SLASH) + trackingPatterns:FrozenSet[str]? = m.getTrackingPatterns() if m.track or m.picky else f{} # for current branch + if '--patterns' in options: + out:str = ajoin("TRK ", [p for p in trackingPatterns if os.path.dirname(p).replace(os.sep, SLASH) == relPath], nl = "\n") + if out: print(out) + return files:List[str] = list(sorted(entry for entry in os.listdir(cwd) if os.path.isfile(entry))) - trackingPatterns:FrozenSet[str] = m.getTrackingPatterns() if m.track or m.picky else f{} # for current branch - for file in files: + for file in files: # for each file list all tracking patterns that match, or none (e.g. in picky mode after commit) ignore:str? = None for ig in m.c.ignores: if fnmatch.fnmatch(file, ig): ignore = ig; break # remember first match TODO document this if ig: for wl in m.c.ignoresWhitelist: if fnmatch.fnmatch(file, wl): ignore = None; break # found a white list entry for ignored file, undo ignoring it - if ignore is None: + if not ignore: matches:List[str] = [] for pattern in (p for p in trackingPatterns if os.path.dirname(p).replace(os.sep, SLASH) == relPath): # only patterns matching current folder - if fnmatch.fnmatch(file, os.path.basename(pattern)): matches.append(pattern) # TODO or only file basename? - print("%s %s%s" % ("IGN" if ignore is not None else ("TRK" if len(matches) > 0 else " "), file, ' by "%s"' % ignore if ignore is not None else (" by " + ";".join(['"%s"' % match for match in matches]) if len(matches) > 0 else ""))) + if fnmatch.fnmatch(file, os.path.basename(pattern)): matches.append(pattern) + print("%s %s%s" % ("IGN" if ignore is not None else ("TRK" if len(matches) > 0 else "..."), file, ' by "%s"' % ignore if ignore is not None else (" by " + ";".join(['"%s"' % match for match in matches]) if len(matches) > 0 else ""))) def log(options:str[]) -> None: ''' List previous commits on current branch. ''' @@ -796,14 +804,15 @@ def move(relPath:str, pattern:str, newRelPath:str, newPattern:str, options:List[ if not (force or soft): Exit("File or glob pattern '%s' is not tracked on current branch. 'sos move' only works on tracked patterns" % pattern) basePattern:str = os.path.basename(pattern) # pure glob without folder newBasePattern:str = os.path.basename(newPattern) - if basePattern.count("*") != newBasePattern.count("*")\ - or (basePattern.count("?") - basePattern.count("[?]")) != (newBasePattern.count("?") - newBasePattern.count("[?]"))\ - or (basePattern.count("[") - basePattern.count("\\[")) != (newBasePattern.count("[") - newBasePattern.count("\\["))\ - or (basePattern.count("]") - basePattern.count("\\]")) != (newBasePattern.count("]") - newBasePattern.count("\\]")): + if basePattern.count("*") < newBasePattern.count("*")\ + or (basePattern.count("?") - basePattern.count("[?]")) < (newBasePattern.count("?") - newBasePattern.count("[?]"))\ + or (basePattern.count("[") - basePattern.count("\\[")) < (newBasePattern.count("[") - newBasePattern.count("\\["))\ + or (basePattern.count("]") - basePattern.count("\\]")) < (newBasePattern.count("]") - newBasePattern.count("\\]")): Exit("Glob markers from '%s' to '%s' don't match, cannot move/rename tracked matching files" % (basePattern, newBasePattern)) oldTokens:GlobBlock[]; newToken:GlobBlock[] oldTokens, newTokens = tokenizeGlobPatterns(os.path.basename(pattern), os.path.basename(newPattern)) matches:Tuple[str,str][] = convertGlobFiles(matching, oldTokens, newTokens) # computes list of source - target filename pairs + if len(s{st[1] for st in matches}) != len(matches): Exit("Some target filenames are not unique and different move/rename actions would point to the same target file") matches = reorderRenameActions(matches, exitOnConflict = not soft) # attempts to find conflict-free renaming order, or exits if os.path.exists(newRelPath): exists:str[] = [filename[1] for filename in matches if os.path.exists(os.path.join(newRelPath, filename[1]).replace(SLASH, os.sep))] @@ -838,8 +847,8 @@ def parse(root:str, cwd:str): elif command[:2] == "di": diff(argument, options, onlys, excps) elif command[:1] == "h": usage() elif command[:2] == "lo": log(options) - elif command[:2] == "li": ls(argument) - elif command[:2] == "ls": ls(argument) + elif command[:2] == "li": ls(argument, options) + elif command[:2] == "ls": ls(argument, options) elif command[:1] == "m": move(relPath, pattern, newRelPath, newPattern, options[1:]) elif command[:2] == "of": offline(argument, options) elif command[:2] == "on": online(options) diff --git a/sos/tests.coco b/sos/tests.coco index 025b751..ac2bbf9 100644 --- a/sos/tests.coco +++ b/sos/tests.coco @@ -62,6 +62,7 @@ def wrapChannels(func: -> Any) = logging.getLogger().addHandler(handler) try: func() # capture output into buf except Exception as E: buf.write(str(E) + "\n"); traceback.print_exc(file = buf) + except SystemExit as F: buf.write(str(F) + "\n"); traceback.print_exc(file = buf) logging.getLogger().removeHandler(handler) sys.argv, sys.stdout, sys.stderr = oldv, oldo, olde buf.getvalue() @@ -596,6 +597,8 @@ class Tests(unittest.TestCase): _.assertInAny('TRK file1 by "./file*"', out) _.assertNotInAny(' file1 by "./file*"', out) _.assertInAny(" foo", out) + out = sos.safeSplit(wrapChannels(() -> sos.ls(options = ["--patterns"])).replace("\r", ""), "\n") + _.assertInAny("TRK ./file*", out) def testCompression(_): _.createFile(1) @@ -733,7 +736,6 @@ class Tests(unittest.TestCase): sos.add(".", "./file2") sos.commit(onlys = f{"./file1"}) _.assertEqual(2, len(os.listdir(branchFolder(0, 1)))) # only meta and file1 - import pdb; pdb.set_trace() sos.commit() # adds also file2 _.assertEqual(2, len(os.listdir(branchFolder(0, 2)))) # only meta and file1 _.createFile(1, "cc") # modify both files @@ -802,7 +804,12 @@ class Tests(unittest.TestCase): sos.commit() _.assertEqual(2, len(os.listdir(branchFolder(0, 1)))) _.assertTrue(os.path.exists(branchFolder(0, 1) + os.sep + "93b38f90892eb5c57779ca9c0b6fbdf6774daeee3342f56f3e78eb2fe5336c50")) # a1b2 - # only rename if actually any files are versioned? or simply what is alife? + _.createFile("1a1b1") + _.createFile("1a1b2") + sos.add(".", "?a?b*") + _.assertIn("not unique", wrapChannels(() -> sos.move(".", "?a?b*", ".", "z?z?"))) # should raise error due to same target name + # TODO only rename if actually any files are versioned? or simply what is alife? + # TODO add test if two single question marks will be moved into adjacent characters def testFindBase(_): old = os.getcwd() diff --git a/sos/utility.coco b/sos/utility.coco index c982bf5..d9fe717 100644 --- a/sos/utility.coco +++ b/sos/utility.coco @@ -113,7 +113,7 @@ try: Splittable = TypeVar("Splittable", str, bytes) except: pass # Python 2 def safeSplit(s:Splittable, d:Splittable? = None) -> Splittable[]: return s.split(d ?? ("\n" if isinstance(s, str) else b"\n")) if len(s) > 0 else [] -def ajoin(sep:str, seq:str[], nl = "") -> str = sep + (nl + sep).join(seq) if seq else "" +def ajoin(sep:str, seq:str[], nl = "") -> str = (sep + (nl + sep).join(seq)) if seq else "" def sjoin(*s:Tuple[Any]) -> str = " ".join([str(e) for e in s if e != '']) @@ -333,7 +333,7 @@ def tokenizeGlobPatterns(oldPattern:str, newPattern:str) -> Tuple[GlobBlock[], G ot:List[GlobBlock] = tokenizeGlobPattern(oldPattern) nt:List[GlobBlock] = tokenizeGlobPattern(newPattern) # if len(ot) != len(nt): Exit("Source and target patterns can't be translated due to differing number of parsed glob markers and literal strings") - if len([o for o in ot if not o.isLiteral]) != len([n for n in nt if not n.isLiteral]): Exit("Source and target file patterns contain differing number of glob markers and can't be translated") + if len([o for o in ot if not o.isLiteral]) < len([n for n in nt if not n.isLiteral]): Exit("Source and target file patterns contain differing number of glob markers and can't be translated") if any(O.content != N.content for O, N in zip([o for o in ot if not o.isLiteral], [n for n in nt if not n.isLiteral])): Exit("Source and target file patterns differ in semantics") (ot, nt)