From bc08f4861a5cbeccd5a1472744478acbcc376e0d Mon Sep 17 00:00:00 2001 From: ArneBachmann Date: Wed, 25 Apr 2018 22:17:45 +0200 Subject: [PATCH] Fixes #227. Fix doc encoding problem. Fixes #77. Fix for find unverlying VCS. --- README.md | 2 +- sos/sos.coco | 35 +-- sos/sos.py | 553 +++++++++++++++++++++++------------------------ sos/tests.coco | 4 +- sos/tests.py | 524 ++++++++++++++++++++++---------------------- sos/usage.coco | 10 +- sos/usage.py | 150 ++++++------- sos/utility.coco | 14 +- sos/utility.py | 34 +-- sos/version.py | 4 +- 10 files changed, 669 insertions(+), 661 deletions(-) diff --git a/README.md b/README.md index 366f50e..74700fb 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ SOS supports three different file handling models that you may use to your likin - Has a small user base as of now, therefore no reliable reports of compatibility and operational capability except for the automatic unit tests run on Travis CI and AppVeyor ### Compatibility ### -- SOS runs on any Python 3.4 distribution or higher, including some versions of PyPy. Python 2 is not supported anymore due to library issues, although SOS's programming language *Coconut* is generally able to transpile to valid Python 2 source code. Use `pip install sos-vcs[backport]` to attemüt running SOS on Python 3.3 or earlier +- SOS runs on any Python 3.4 distribution or higher, including some versions of PyPy. Python 2 is not supported anymore due to library issues, although SOS's programming language *Coconut* is generally able to transpile to valid Python 2 source code. Use `pip install sos-vcs[backport]` to attempt running SOS on Python 3.3 or earlier - SOS is compatible with above mentioned traditional VCSs: SVN, Git, gitless, Bazaar, Mercurial and Fossil - Filename encoding and console encoding: Full roundtrip support (on Windows) started only with Python 3.6.4 and has not been tested nor confirmed yet for SOS diff --git a/sos/sos.coco b/sos/sos.coco index 6d89f1c..987b8ca 100644 --- a/sos/sos.coco +++ b/sos/sos.coco @@ -555,8 +555,8 @@ def _diff(m:Metadata, branch:int, revision:int, changed:ChangeSet, ignoreWhitesp elif block.tipe == MergeBlockType.REMOVE: for no, line in enumerate(block.lines): printo(wrap("+++ %%0%dd |%%s|" % linemax % (no + block.line, line))) elif block.tipe == MergeBlockType.REPLACE: - for no, line in enumerate(block.replaces.lines): printo(wrap("- | %%0%dd |%%s|" % linemax % (no + block.replaces.line, line))) - for no, line in enumerate(block.lines): printo(wrap("+ | %%0%dd |%%s|" % linemax % (no + block.line, line))) + for no, line in enumerate(block.replaces.lines): printo(wrap("-~- %%0%dd |%%s|" % linemax % (no + block.replaces.line, line))) + for no, line in enumerate(block.lines): printo(wrap("+~+ %%0%dd |%%s|" % linemax % (no + block.line, line))) # elif block.tipe == MergeBlockType.KEEP: pass # TODO allow to show kept stuff, or a part of pre-post lines # elif block.tipe == MergeBlockType.MOVE: # intra-line modifications if block.tipe != MergeBlockType.KEEP: printo() @@ -854,7 +854,7 @@ def ls(folder:str? = None, options:str[] = []): if '--all' in options: folder = m.root # always start at SOS repo root with --all recursive:bool = '--recursive' in options or '-r' in options or '--all' in options patterns:bool = '--patterns' in options or '-p' in options - DOT:str = (DOT_SYMBOL if m.c.useUnicodeFont else " ") * 3 + DOT:str = (DOT_SYMBOL if m.c.useUnicodeFont else " ") * 3 # TODO or "."? if verbose: info(usage.MARKER + "Repository is in %s mode" % ("tracking" if m.track else ("picky" if m.picky else "simple"))) relPath:str = relativize(m.root, os.path.join(folder, "-"))[0] if relPath.startswith(os.pardir): Exit("Cannot list contents of folder outside offline repository") @@ -972,25 +972,26 @@ def dump(argument:str, options:str[] = []): def publish(message:str?, cmd:str, options:str[] = [], onlys:FrozenSet[str]? = None, excps:FrozenSet[str]? = None): ''' Write changes made to the branch into one commit of the underlying VCS without further checks. ''' m:Metadata = Metadata() - if not (m.track or m.picky): Exit("Not implemented for simple repository mode yet") # TODO add manual file picking mode (add by extension, recursive, ... see issue for that) + if not (m.track or m.picky): Exit("Not implemented for simple repository mode yet") # TODO add manual file picking mode instead (add by extension, recursive, ... see issue for details) m, branch, revision, changed, strict, force, trackingPatterns, untrackingPatterns = exitOnChanges(None, options, onlys = onlys, excps = excps) maxi:int? = m.getHighestRevision(branch) - if maxi is None: Exit("No revision to publish on current (or any parent-) branch") + if maxi is None: Exit("No revision to publish on current branch (or any of its parents after fast-branching)") m.computeSequentialPathSet(branch, maxi) # load all commits up to specified revision - # HINT logic to only add changed files vs. originating file state - would require in-depth underlying VCS knowledge, probably out of scope, or assume commit 0 as base (that's what we currently do) - import subprocess # only required in this section - # HINT stash/rollback for Git? or implement a global mechanism? Actually there's nothing to backup, as nothing is really changedon the FS + # HINT logic to only add changed files vs. originating file state - would require in-depth underlying VCS knowledge. We currentöy assume commit 0 as base # TODO discuss: only commit changes from r1.. onward vs. r0?, or attempt to add everything in repo, even if unchanged? the problem is that for different branches we might need to switch also underlying branches - for path, pinfo in m.paths.items(): - command:str = fitStrings(list(m.paths.keys()), prefix = "%s add" % cmd) # considering maximum command-line length, filename quoting, and spaces -# returncode:int = subprocess.Popen(command, shell = False).wait() - returncode:int = 0; printo(command) #, shell = False) # TODO - if returncode != 0: Exit("Error adding files from SOS revision to underlying VCS. Leaving in inconsistent %s state" % vcsNames[cmd]) + import subprocess # only required in this section + # HINT stash/rollback for Git? or implement a global mechanism to revert? + files:str[] = list(m.paths.keys()) + while files: + command:str = fitStrings(files, prefix = "%s add" % cmd, process = -> '"%s"' % _.replace("\"", "\\\"")) # considering maximum command-line length, filename quoting, and spaces + returncode:int = subprocess.Popen(command, shell = False).wait() +# returncode:int = 0; debug(command) + if returncode != 0: Exit("Error adding files from SOS revision to underlying VCS. Leaving %s in potentially inconsistent state" % vcsNames[cmd]) tracked:bool; commitArgs:str?; tracked, commitArgs = vcsCommits[cmd] - #returncode = subprocess.Popen('%s commit -m "%s" %s' % (cmd, message ?? "Committed from SOS branch/revision %s/r%02d on %s" % (strftime(now)).replace("\"", "\\\""), _.branches[branch].name ?? ("b%d" % _.branch), revision, commitArgs ?? "")).wait() # TODO quote-escaping on Windows - printo('%s commit -m "%s" %s' % (cmd, message ?? "Committed from SOS branch/revision %s/r%02d on %s" % (m.branches[branch].name ?? ("b%d" % m.branch), revision, strftime()).replace("\"", "\\\""), commitArgs ?? "")) # TODO quote-escaping on Windows + returncode = subprocess.Popen(('%s commit -m "%s" %s' % (cmd, message ?? ("Committed from SOS %s/r%02d on %s" % (m.branches[branch].name ?? ("b%d" % m.branch), revision, strftime())).replace("\"", "\\\""), commitArgs ?? ""))) # TODO quote-escaping on Windows +# debug(('%s commit -m "%s" %s' % (cmd, message ?? ("Committed from SOS %s/r%02d on %s" % (m.branches[branch].name ?? ("b%d" % m.branch), revision, strftime())).replace("\"", "\\\""), commitArgs ?? ""))) if returncode != 0: Exit("Error committing files from SOS revision to underlying VCS. Please check current %s state" % cmd) - if tracked: printo("Please note that all the files added in this commit will continue to be tracked by the underlying VCS") + if tracked: warn("Please note that all the files added in this commit will continue to be tracked by the underlying VCS") def config(arguments:List[str], options:List[str] = []): command:str; key:str; value:str; v:str @@ -1144,7 +1145,7 @@ def main(): debug("Detected SOS root folder: %s\nDetected VCS root folder: %s" % (root ?? "-", vcs ?? "-")) defaults["defaultbranch"] = vcsBranches.get(cmd, vcsBranches[SVN]) ?? "default" # sets dynamic default with SVN fallback defaults["useChangesCommand"] = cmd == "fossil" # sets dynamic default with SVN fallback - if force_sos or root is not None or (command ?? "")[:2] == "of" or (command ?? "")[:1] in "hv": # in offline mode or just going offline TODO what about git config? + if (not force_vcs or force_sos) and (root is not None or (command ?? "")[:2] == "of" or (command ?? "_")[:1] in "hv"): # in offline mode or just going offline TODO what about git config? cwd = os.getcwd() os.chdir(cwd if command[:2] == "of" else root ?? cwd) parse(vcs, cwd, cmd) diff --git a/sos/sos.py b/sos/sos.py index 428c5c4..c3e2ff9 100644 --- a/sos/sos.py +++ b/sos/sos.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# __coconut_hash__ = 0x85d7bef8 +# __coconut_hash__ = 0x89ff4c26 # Compiled with Coconut version 1.3.1-post_dev28 [Dead Parrot] @@ -680,9 +680,9 @@ def _diff(m: 'Metadata', branch: 'int', revision: 'int', changed: 'ChangeSet', i printo(wrap("+++ %%0%dd |%%s|" % linemax % (no + block.line, line))) # line 556 elif block.tipe == MergeBlockType.REPLACE: # line 557 for no, line in enumerate(block.replaces.lines): # line 558 - printo(wrap("- | %%0%dd |%%s|" % linemax % (no + block.replaces.line, line))) # line 558 + printo(wrap("-~- %%0%dd |%%s|" % linemax % (no + block.replaces.line, line))) # line 558 for no, line in enumerate(block.lines): # line 559 - printo(wrap("+ | %%0%dd |%%s|" % linemax % (no + block.line, line))) # line 559 + printo(wrap("+~+ %%0%dd |%%s|" % linemax % (no + block.line, line))) # line 559 # elif block.tipe == MergeBlockType.KEEP: pass # TODO allow to show kept stuff, or a part of pre-post lines # elif block.tipe == MergeBlockType.MOVE: # intra-line modifications if block.tipe != MergeBlockType.KEEP: # line 562 @@ -1018,7 +1018,7 @@ def ls(folder: '_coconut.typing.Optional[str]'=None, options: '_coconut.typing.S folder = m.root # always start at SOS repo root with --all # line 854 recursive = '--recursive' in options or '-r' in options or '--all' in options # type: bool # line 855 patterns = '--patterns' in options or '-p' in options # type: bool # line 856 - DOT = (DOT_SYMBOL if m.c.useUnicodeFont else " ") * 3 # type: str # line 857 + DOT = (DOT_SYMBOL if m.c.useUnicodeFont else " ") * 3 # type: str # TODO or "."? # line 857 if verbose: # line 858 info(usage.MARKER + "Repository is in %s mode" % ("tracking" if m.track else ("picky" if m.picky else "simple"))) # line 858 relPath = relativize(m.root, os.path.join(folder, "-"))[0] # type: str # line 859 @@ -1172,288 +1172,287 @@ def dump(argument: 'str', options: '_coconut.typing.Sequence[str]'=[]): # line def publish(message: '_coconut.typing.Optional[str]', cmd: 'str', options: '_coconut.typing.Sequence[str]'=[], onlys: '_coconut.typing.Optional[FrozenSet[str]]'=None, excps: '_coconut.typing.Optional[FrozenSet[str]]'=None): # line 972 ''' Write changes made to the branch into one commit of the underlying VCS without further checks. ''' # line 973 m = Metadata() # type: Metadata # line 974 - if not (m.track or m.picky): # TODO add manual file picking mode (add by extension, recursive, ... see issue for that) # line 975 - Exit("Not implemented for simple repository mode yet") # TODO add manual file picking mode (add by extension, recursive, ... see issue for that) # line 975 + # if not (m.track or m.picky): # TODO add manual file picking mode instead (add by extension, recursive, ... see issue for details) # line 975 + # Exit("Not implemented for simple repository mode yet") # TODO add manual file picking mode instead (add by extension, recursive, ... see issue for details) # line 975 m, branch, revision, changed, strict, force, trackingPatterns, untrackingPatterns = exitOnChanges(None, options, onlys=onlys, excps=excps) # line 976 maxi = m.getHighestRevision(branch) # type: _coconut.typing.Optional[int] # line 977 if maxi is None: # line 978 - Exit("No revision to publish on current (or any parent-) branch") # line 978 + Exit("No revision to publish on current branch (or any of its parents after fast-branching)") # line 978 m.computeSequentialPathSet(branch, maxi) # load all commits up to specified revision # line 979 -# HINT logic to only add changed files vs. originating file state - would require in-depth underlying VCS knowledge, probably out of scope, or assume commit 0 as base (that's what we currently do) - import subprocess # only required in this section # line 981 -# HINT stash/rollback for Git? or implement a global mechanism? Actually there's nothing to backup, as nothing is really changedon the FS +# HINT logic to only add changed files vs. originating file state - would require in-depth underlying VCS knowledge. We currentöy assume commit 0 as base # TODO discuss: only commit changes from r1.. onward vs. r0?, or attempt to add everything in repo, even if unchanged? the problem is that for different branches we might need to switch also underlying branches - for path, pinfo in m.paths.items(): # line 984 - command = fitStrings(list(m.paths.keys()), prefix="%s add" % cmd) # type: str # considering maximum command-line length, filename quoting, and spaces # line 985 + import subprocess # only required in this section # line 982 +# HINT stash/rollback for Git? or implement a global mechanism to revert? + command = fitStrings(list(m.paths.keys()), prefix="%s add" % cmd, process=lambda _=None: '"%s"' % _.replace("\"", "\\\"")) # type: str # considering maximum command-line length, filename quoting, and spaces # line 984 # returncode:int = subprocess.Popen(command, shell = False).wait() - returncode = 0 # type: int #, shell = False) # TODO # line 987 - printo(command) #, shell = False) # TODO # line 987 - if returncode != 0: # line 988 - Exit("Error adding files from SOS revision to underlying VCS. Leaving in inconsistent %s state" % vcsNames[cmd]) # line 988 - tracked = None # type: bool # line 989 - commitArgs = None # type: _coconut.typing.Optional[str] # line 989 - tracked, commitArgs = vcsCommits[cmd] # line 989 -#returncode = subprocess.Popen('%s commit -m "%s" %s' % (cmd, message ?? "Committed from SOS branch/revision %s/r%02d on %s" % (strftime(now)).replace("\"", "\\\""), _.branches[branch].name ?? ("b%d" % _.branch), revision, commitArgs ?? "")).wait() # TODO quote-escaping on Windows - printo('%s commit -m "%s" %s' % (cmd, ("Committed from SOS branch/revision %s/r%02d on %s" % ((lambda _coconut_none_coalesce_item: ("b%d" % m.branch) if _coconut_none_coalesce_item is None else _coconut_none_coalesce_item)(m.branches[branch].name), revision, strftime()).replace("\"", "\\\"") if message is None else message), ("" if commitArgs is None else commitArgs))) # TODO quote-escaping on Windows # line 991 - if returncode != 0: # line 992 - Exit("Error committing files from SOS revision to underlying VCS. Please check current %s state" % cmd) # line 992 - if tracked: # line 993 - printo("Please note that all the files added in this commit will continue to be tracked by the underlying VCS") # line 993 - -def config(arguments: 'List[str]', options: 'List[str]'=[]): # line 995 - command = None # type: str # line 996 - key = None # type: str # line 996 - value = None # type: str # line 996 - v = None # type: str # line 996 - command, key, value = (arguments + [None] * 2)[:3] # line 997 - if command is None: # line 998 - usage.usage("help", verbose=True) # line 998 - if command not in ["set", "unset", "show", "list", "add", "rm"]: # line 999 - Exit("Unknown config command") # line 999 - local = "--local" in options # type: bool # line 1000 - m = Metadata() # type: Metadata # loads layered configuration as well. TODO warning if repo not exists # line 1001 - c = m.c if local else m.c.__defaults # type: configr.Configr # line 1002 - if command == "set": # line 1003 - if None in (key, value): # line 1004 - Exit("Key or value not specified") # line 1004 - if key not in (([] if local else CONFIGURABLE_FLAGS + ["defaultbranch"]) + CONFIGURABLE_LISTS + CONFIGURABLE_INTS): # TODO move defaultbranch to configurable_texts? # line 1005 - Exit("Unsupported key for %s configuration %r" % ("local " if local else "global", key)) # TODO move defaultbranch to configurable_texts? # line 1005 - if key in CONFIGURABLE_FLAGS and value.lower() not in TRUTH_VALUES + FALSE_VALUES: # line 1006 - Exit("Cannot set flag to '%s'. Try on/off instead" % value.lower()) # line 1006 - c[key] = value.lower() in TRUTH_VALUES if key in CONFIGURABLE_FLAGS else (tryOrIgnore(lambda _=None: int(value), lambda E: error("Not an integer value: %r" % E)) if key in CONFIGURABLE_INTS else (removePath(key, value.strip()) if key not in CONFIGURABLE_LISTS else [removePath(key, v) for v in safeSplit(value, ";")])) # TODO sanitize texts? # line 1007 - elif command == "unset": # line 1008 - if key is None: # line 1009 - Exit("No key specified") # line 1009 - if key not in c.keys(): # HINT: Works on local configurations when used with --local # line 1010 - Exit("Unknown key") # HINT: Works on local configurations when used with --local # line 1010 - del c[key] # line 1011 - elif command == "add": # line 1012 - if None in (key, value): # line 1013 - Exit("Key or value not specified") # line 1013 - if key not in CONFIGURABLE_LISTS: # line 1014 - Exit("Unsupported key %r" % key) # line 1014 - if key not in c.keys(): # prepare empty list, or copy from global, add new value below # line 1015 - c[key] = [_ for _ in c.__defaults[key]] if local else [] # prepare empty list, or copy from global, add new value below # line 1015 - elif value in c[key]: # line 1016 - Exit("Value already contained, nothing to do") # line 1016 - if ";" in value: # line 1017 - c[key].append(removePath(key, value)) # line 1017 - else: # line 1018 - c[key].extend([removePath(key, v) for v in value.split(";")]) # line 1018 - elif command == "rm": # line 1019 - if None in (key, value): # line 1020 - Exit("Key or value not specified") # line 1020 - if key not in c.keys(): # line 1021 - Exit("Unknown key %r" % key) # line 1021 - if value not in c[key]: # line 1022 - Exit("Unknown value %r" % value) # line 1022 - c[key].remove(value) # line 1023 - if local and len(c[key]) == 0 and "--prune" in options: # remove local entry, to fallback to global # line 1024 - del c[key] # remove local entry, to fallback to global # line 1024 - else: # Show or list # line 1025 - if key == "ints": # list valid configuration items # line 1026 - printo(", ".join(CONFIGURABLE_INTS)) # list valid configuration items # line 1026 - elif key == "flags": # line 1027 - printo(", ".join(CONFIGURABLE_FLAGS)) # line 1027 - elif key == "lists": # line 1028 - printo(", ".join(CONFIGURABLE_LISTS)) # line 1028 - elif key == "texts": # line 1029 - printo(", ".join([_ for _ in defaults.keys() if _ not in (CONFIGURABLE_FLAGS + CONFIGURABLE_LISTS)])) # line 1029 - else: # line 1030 - out = {3: "[default]", 2: "[global] ", 1: "[local] "} # type: Dict[int, str] # in contrast to Git, we don't need (nor want) to support a "system" config scope # line 1031 - c = m.c # always use full configuration chain # line 1032 - try: # attempt single key # line 1033 - assert key is not None # force exception # line 1034 - c[key] # force exception # line 1034 - l = key in c.keys() # type: bool # line 1035 - g = key in c.__defaults.keys() # type: bool # line 1035 - printo("%s %s %r" % (key.rjust(20), out[3] if not (l or g) else (out[1] if l else out[2]), c[key])) # line 1036 - except: # normal value listing # line 1037 - vals = {k: (repr(v), 3) for k, v in defaults.items()} # type: Dict[str, Tuple[str, int]] # line 1038 - vals.update({k: (repr(v), 2) for k, v in c.__defaults.items()}) # line 1039 - vals.update({k: (repr(v), 1) for k, v in c.__map.items()}) # line 1040 - for k, vt in sorted(vals.items()): # line 1041 - printo("%s %s %s" % (k.rjust(20), out[vt[1]], vt[0])) # line 1041 - if len(c.keys()) == 0: # line 1042 - info("No local configuration stored") # line 1042 - if len(c.__defaults.keys()) == 0: # line 1043 - info("No global configuration stored") # line 1043 - return # in case of list, no need to store anything # line 1044 - if local: # saves changes of repoConfig # line 1045 - m.repoConf = c.__map # saves changes of repoConfig # line 1045 - m.saveBranches() # saves changes of repoConfig # line 1045 - Exit("OK", code=0) # saves changes of repoConfig # line 1045 - else: # global config # line 1046 - f, h = saveConfig(c) # only saves c.__defaults (nested Configr) # line 1047 - if f is None: # line 1048 - error("Error saving user configuration: %r" % h) # line 1048 - else: # line 1049 - Exit("OK", code=0) # line 1049 - -def move(relPath: 'str', pattern: 'str', newRelPath: 'str', newPattern: 'str', options: 'List[str]'=[], negative: 'bool'=False): # line 1051 + returncode = 0 # type: int #, shell = False) # TODO # line 986 + printo(command) #, shell = False) # TODO # line 986 + if returncode != 0: # line 987 + Exit("Error adding files from SOS revision to underlying VCS. Leaving %s in potentially inconsistent state" % vcsNames[cmd]) # line 987 + tracked = None # type: bool # line 988 + commitArgs = None # type: _coconut.typing.Optional[str] # line 988 + tracked, commitArgs = vcsCommits[cmd] # line 988 +#returncode = subprocess.Popen(('%s commit -m "%s" %s' % (cmd, message ?? "Committed from SOS %s/r%02d on %s" % (m.branches[branch].name ?? ("b%d" % m.branch), revision, strftime()).replace("\"", "\\\""), commitArgs ?? ""))) # TODO quote-escaping on Windows + printo(('%s commit -m "%s" %s' % (cmd, (("Committed from SOS %s/r%02d on %s" % ((lambda _coconut_none_coalesce_item: ("b%d" % m.branch) if _coconut_none_coalesce_item is None else _coconut_none_coalesce_item)(m.branches[branch].name), revision, strftime())).replace("\"", "\\\"") if message is None else message), ("" if commitArgs is None else commitArgs)))) # line 990 + if returncode != 0: # line 991 + Exit("Error committing files from SOS revision to underlying VCS. Please check current %s state" % cmd) # line 991 + if tracked: # line 992 + printo("Please note that all the files added in this commit will continue to be tracked by the underlying VCS") # line 992 + +def config(arguments: 'List[str]', options: 'List[str]'=[]): # line 994 + command = None # type: str # line 995 + key = None # type: str # line 995 + value = None # type: str # line 995 + v = None # type: str # line 995 + command, key, value = (arguments + [None] * 2)[:3] # line 996 + if command is None: # line 997 + usage.usage("help", verbose=True) # line 997 + if command not in ["set", "unset", "show", "list", "add", "rm"]: # line 998 + Exit("Unknown config command") # line 998 + local = "--local" in options # type: bool # line 999 + m = Metadata() # type: Metadata # loads layered configuration as well. TODO warning if repo not exists # line 1000 + c = m.c if local else m.c.__defaults # type: configr.Configr # line 1001 + if command == "set": # line 1002 + if None in (key, value): # line 1003 + Exit("Key or value not specified") # line 1003 + if key not in (([] if local else CONFIGURABLE_FLAGS + ["defaultbranch"]) + CONFIGURABLE_LISTS + CONFIGURABLE_INTS): # TODO move defaultbranch to configurable_texts? # line 1004 + Exit("Unsupported key for %s configuration %r" % ("local " if local else "global", key)) # TODO move defaultbranch to configurable_texts? # line 1004 + if key in CONFIGURABLE_FLAGS and value.lower() not in TRUTH_VALUES + FALSE_VALUES: # line 1005 + Exit("Cannot set flag to '%s'. Try on/off instead" % value.lower()) # line 1005 + c[key] = value.lower() in TRUTH_VALUES if key in CONFIGURABLE_FLAGS else (tryOrIgnore(lambda _=None: int(value), lambda E: error("Not an integer value: %r" % E)) if key in CONFIGURABLE_INTS else (removePath(key, value.strip()) if key not in CONFIGURABLE_LISTS else [removePath(key, v) for v in safeSplit(value, ";")])) # TODO sanitize texts? # line 1006 + elif command == "unset": # line 1007 + if key is None: # line 1008 + Exit("No key specified") # line 1008 + if key not in c.keys(): # HINT: Works on local configurations when used with --local # line 1009 + Exit("Unknown key") # HINT: Works on local configurations when used with --local # line 1009 + del c[key] # line 1010 + elif command == "add": # line 1011 + if None in (key, value): # line 1012 + Exit("Key or value not specified") # line 1012 + if key not in CONFIGURABLE_LISTS: # line 1013 + Exit("Unsupported key %r" % key) # line 1013 + if key not in c.keys(): # prepare empty list, or copy from global, add new value below # line 1014 + c[key] = [_ for _ in c.__defaults[key]] if local else [] # prepare empty list, or copy from global, add new value below # line 1014 + elif value in c[key]: # line 1015 + Exit("Value already contained, nothing to do") # line 1015 + if ";" in value: # line 1016 + c[key].append(removePath(key, value)) # line 1016 + else: # line 1017 + c[key].extend([removePath(key, v) for v in value.split(";")]) # line 1017 + elif command == "rm": # line 1018 + if None in (key, value): # line 1019 + Exit("Key or value not specified") # line 1019 + if key not in c.keys(): # line 1020 + Exit("Unknown key %r" % key) # line 1020 + if value not in c[key]: # line 1021 + Exit("Unknown value %r" % value) # line 1021 + c[key].remove(value) # line 1022 + if local and len(c[key]) == 0 and "--prune" in options: # remove local entry, to fallback to global # line 1023 + del c[key] # remove local entry, to fallback to global # line 1023 + else: # Show or list # line 1024 + if key == "ints": # list valid configuration items # line 1025 + printo(", ".join(CONFIGURABLE_INTS)) # list valid configuration items # line 1025 + elif key == "flags": # line 1026 + printo(", ".join(CONFIGURABLE_FLAGS)) # line 1026 + elif key == "lists": # line 1027 + printo(", ".join(CONFIGURABLE_LISTS)) # line 1027 + elif key == "texts": # line 1028 + printo(", ".join([_ for _ in defaults.keys() if _ not in (CONFIGURABLE_FLAGS + CONFIGURABLE_LISTS)])) # line 1028 + else: # line 1029 + out = {3: "[default]", 2: "[global] ", 1: "[local] "} # type: Dict[int, str] # in contrast to Git, we don't need (nor want) to support a "system" config scope # line 1030 + c = m.c # always use full configuration chain # line 1031 + try: # attempt single key # line 1032 + assert key is not None # force exception # line 1033 + c[key] # force exception # line 1033 + l = key in c.keys() # type: bool # line 1034 + g = key in c.__defaults.keys() # type: bool # line 1034 + printo("%s %s %r" % (key.rjust(20), out[3] if not (l or g) else (out[1] if l else out[2]), c[key])) # line 1035 + except: # normal value listing # line 1036 + vals = {k: (repr(v), 3) for k, v in defaults.items()} # type: Dict[str, Tuple[str, int]] # line 1037 + vals.update({k: (repr(v), 2) for k, v in c.__defaults.items()}) # line 1038 + vals.update({k: (repr(v), 1) for k, v in c.__map.items()}) # line 1039 + for k, vt in sorted(vals.items()): # line 1040 + printo("%s %s %s" % (k.rjust(20), out[vt[1]], vt[0])) # line 1040 + if len(c.keys()) == 0: # line 1041 + info("No local configuration stored") # line 1041 + if len(c.__defaults.keys()) == 0: # line 1042 + info("No global configuration stored") # line 1042 + return # in case of list, no need to store anything # line 1043 + if local: # saves changes of repoConfig # line 1044 + m.repoConf = c.__map # saves changes of repoConfig # line 1044 + m.saveBranches() # saves changes of repoConfig # line 1044 + Exit("OK", code=0) # saves changes of repoConfig # line 1044 + else: # global config # line 1045 + f, h = saveConfig(c) # only saves c.__defaults (nested Configr) # line 1046 + if f is None: # line 1047 + error("Error saving user configuration: %r" % h) # line 1047 + else: # line 1048 + Exit("OK", code=0) # line 1048 + +def move(relPath: 'str', pattern: 'str', newRelPath: 'str', newPattern: 'str', options: 'List[str]'=[], negative: 'bool'=False): # line 1050 ''' Path differs: Move files, create folder if not existing. Pattern differs: Attempt to rename file, unless exists in target or not unique. for "mvnot" don't do any renaming (or do?) - ''' # line 1054 - if verbose: # line 1055 - info(usage.MARKER + "Renaming %r to %r" % (pattern, newPattern)) # line 1055 - force = '--force' in options # type: bool # line 1056 - soft = '--soft' in options # type: bool # line 1057 - if not os.path.exists(encode(relPath.replace(SLASH, os.sep))) and not force: # line 1058 - Exit("Source folder doesn't exist. Use --force to proceed anyway") # line 1058 - m = Metadata() # type: Metadata # line 1059 - patterns = m.branches[m.branch].untracked if negative else m.branches[m.branch].tracked # type: List[str] # line 1060 - matching = fnmatch.filter(os.listdir(relPath.replace(SLASH, os.sep)) if os.path.exists(encode(relPath.replace(SLASH, os.sep))) else [], os.path.basename(pattern)) # type: List[str] # find matching files in source # line 1061 - matching[:] = [f for f in matching if len([n for n in m.c.ignores if fnmatch.fnmatch(f, n)]) == 0 or len([p for p in m.c.ignoresWhitelist if fnmatch.fnmatch(f, p)]) > 0] # line 1062 - if not matching and not force: # line 1063 - Exit("No files match the specified file pattern. Use --force to proceed anyway") # line 1063 - if not (m.track or m.picky): # line 1064 - Exit("Repository is in simple mode. Simply use basic file operations to modify files, then execute 'sos commit' to version the changes") # line 1064 - if pattern not in patterns: # list potential alternatives and exit # line 1065 - for tracked in (t for t in patterns if os.path.dirname(t) == relPath): # for all patterns of the same source folder # line 1066 - alternative = fnmatch.filter(matching, os.path.basename(tracked)) # type: _coconut.typing.Sequence[str] # find if it matches any of the files in the source folder, too # line 1067 - if alternative: # line 1068 - info(" '%s' matches %d files" % (tracked, len(alternative))) # line 1068 - if not (force or soft): # line 1069 - Exit("File pattern '%s' is not tracked on current branch. 'sos move' only works on tracked patterns" % pattern) # line 1069 - basePattern = os.path.basename(pattern) # type: str # pure glob without folder # line 1070 - newBasePattern = os.path.basename(newPattern) # type: str # line 1071 - 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("\\]")): # line 1072 - Exit("Glob markers from '%s' to '%s' don't match, cannot move/rename tracked matching files" % (basePattern, newBasePattern)) # line 1076 - oldTokens = None # type: _coconut.typing.Sequence[GlobBlock] # line 1077 - newToken = None # type: _coconut.typing.Sequence[GlobBlock] # line 1077 - oldTokens, newTokens = tokenizeGlobPatterns(os.path.basename(pattern), os.path.basename(newPattern)) # line 1078 - matches = convertGlobFiles(matching, oldTokens, newTokens) # type: _coconut.typing.Sequence[Tuple[str, str]] # computes list of source - target filename pairs # line 1079 - if len({st[1] for st in matches}) != len(matches): # line 1080 - Exit("Some target filenames are not unique and different move/rename actions would point to the same target file") # line 1080 - matches = reorderRenameActions(matches, exitOnConflict=not soft) # attempts to find conflict-free renaming order, or exits # line 1081 - if os.path.exists(encode(newRelPath)): # line 1082 - exists = [filename[1] for filename in matches if os.path.exists(encode(os.path.join(newRelPath, filename[1]).replace(SLASH, os.sep)))] # type: _coconut.typing.Sequence[str] # line 1083 - if exists and not (force or soft): # line 1084 - Exit("%s files would write over existing files in %s cases. Use --force to execute it anyway" % ("Moving" if relPath != newRelPath else "Renaming", "all" if len(exists) == len(matches) else "some")) # line 1084 - else: # line 1085 - os.makedirs(encode(os.path.abspath(newRelPath.replace(SLASH, os.sep)))) # line 1085 - if not soft: # perform actual renaming # line 1086 - for (source, target) in matches: # line 1087 - try: # line 1088 - shutil.move(encode(os.path.abspath(os.path.join(relPath, source).replace(SLASH, os.sep))), encode(os.path.abspath(os.path.join(newRelPath, target).replace(SLASH, os.sep)))) # line 1088 - except Exception as E: # one error can lead to another in case of delicate renaming order # line 1089 - error("Cannot move/rename file '%s' to '%s'" % (source, os.path.join(newRelPath, target))) # one error can lead to another in case of delicate renaming order # line 1089 - patterns[patterns.index(pattern)] = newPattern # line 1090 - m.saveBranches() # line 1091 - -def parse(vcs: 'str', cwd: 'str', cmd: 'str'): # line 1093 - ''' Main operation. root is underlying VCS base dir. main() has already chdir'ed into SOS root folder, cwd is original working directory for add, rm, mv. ''' # line 1094 - debug("Parsing command-line arguments...") # line 1095 - root = os.getcwd() # line 1096 - try: # line 1097 - onlys, excps = parseOnlyOptions(cwd, sys.argv) # extracts folder-relative paths (used in changes, commit, diff, switch, update) # line 1098 - command = sys.argv[1].strip() if len(sys.argv) > 1 else "" # line 1099 - arguments = [c.strip() for c in sys.argv[2:] if not (c.startswith("-") and (len(c) == 2 or c[1] == "-"))] # type: List[_coconut.typing.Optional[str]] # line 1100 - options = [c.strip() for c in sys.argv[2:] if c.startswith("-") and (len(c) == 2 or c[1] == "-")] # options with arguments have to be parsed from sys.argv # line 1101 - debug("Processing command %r with arguments %r and options %r." % (command, [_ for _ in arguments if _ is not None], options)) # line 1102 - if command[:1] in "amr": # line 1103 - relPath, pattern = relativize(root, os.path.join(cwd, arguments[0] if arguments else ".")) # line 1103 - if command[:1] == "m": # line 1104 - if len(arguments) < 2: # line 1105 - Exit("Need a second file pattern argument as target for move command") # line 1105 - newRelPath, newPattern = relativize(root, os.path.join(cwd, arguments[1])) # line 1106 - arguments[:] = (arguments + [None] * 3)[:3] # line 1107 - if command[:1] == "a": # addnot # line 1108 - add(relPath, pattern, options, negative="n" in command) # addnot # line 1108 - elif command[:1] == "b": # line 1109 - branch(arguments[0], arguments[1], options) # line 1109 - elif command[:3] == "com": # line 1110 - commit(arguments[0], options, onlys, excps) # line 1110 - elif command[:2] == "ch": # "changes" (legacy) # line 1111 - changes(arguments[0], options, onlys, excps) # "changes" (legacy) # line 1111 - elif command[:2] == "ci": # line 1112 - commit(arguments[0], options, onlys, excps) # line 1112 - elif command[:3] == 'con': # line 1113 - config(arguments, options) # line 1113 - elif command[:2] == "de": # line 1114 - destroy(arguments[0], options) # line 1114 - elif command[:2] == "di": # line 1115 - diff(arguments[0], options, onlys, excps) # line 1115 - elif command[:2] == "du": # line 1116 - dump(arguments[0], options) # line 1116 - elif command[:1] == "h": # line 1117 - usage.usage(arguments[0], verbose=verbose) # line 1117 - elif command[:2] == "lo": # line 1118 - log(options) # line 1118 - elif command[:2] == "li": # line 1119 + ''' # line 1053 + if verbose: # line 1054 + info(usage.MARKER + "Renaming %r to %r" % (pattern, newPattern)) # line 1054 + force = '--force' in options # type: bool # line 1055 + soft = '--soft' in options # type: bool # line 1056 + if not os.path.exists(encode(relPath.replace(SLASH, os.sep))) and not force: # line 1057 + Exit("Source folder doesn't exist. Use --force to proceed anyway") # line 1057 + m = Metadata() # type: Metadata # line 1058 + patterns = m.branches[m.branch].untracked if negative else m.branches[m.branch].tracked # type: List[str] # line 1059 + matching = fnmatch.filter(os.listdir(relPath.replace(SLASH, os.sep)) if os.path.exists(encode(relPath.replace(SLASH, os.sep))) else [], os.path.basename(pattern)) # type: List[str] # find matching files in source # line 1060 + matching[:] = [f for f in matching if len([n for n in m.c.ignores if fnmatch.fnmatch(f, n)]) == 0 or len([p for p in m.c.ignoresWhitelist if fnmatch.fnmatch(f, p)]) > 0] # line 1061 + if not matching and not force: # line 1062 + Exit("No files match the specified file pattern. Use --force to proceed anyway") # line 1062 + if not (m.track or m.picky): # line 1063 + Exit("Repository is in simple mode. Simply use basic file operations to modify files, then execute 'sos commit' to version the changes") # line 1063 + if pattern not in patterns: # list potential alternatives and exit # line 1064 + for tracked in (t for t in patterns if os.path.dirname(t) == relPath): # for all patterns of the same source folder # line 1065 + alternative = fnmatch.filter(matching, os.path.basename(tracked)) # type: _coconut.typing.Sequence[str] # find if it matches any of the files in the source folder, too # line 1066 + if alternative: # line 1067 + info(" '%s' matches %d files" % (tracked, len(alternative))) # line 1067 + if not (force or soft): # line 1068 + Exit("File pattern '%s' is not tracked on current branch. 'sos move' only works on tracked patterns" % pattern) # line 1068 + basePattern = os.path.basename(pattern) # type: str # pure glob without folder # line 1069 + newBasePattern = os.path.basename(newPattern) # type: str # line 1070 + 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("\\]")): # line 1071 + Exit("Glob markers from '%s' to '%s' don't match, cannot move/rename tracked matching files" % (basePattern, newBasePattern)) # line 1075 + oldTokens = None # type: _coconut.typing.Sequence[GlobBlock] # line 1076 + newToken = None # type: _coconut.typing.Sequence[GlobBlock] # line 1076 + oldTokens, newTokens = tokenizeGlobPatterns(os.path.basename(pattern), os.path.basename(newPattern)) # line 1077 + matches = convertGlobFiles(matching, oldTokens, newTokens) # type: _coconut.typing.Sequence[Tuple[str, str]] # computes list of source - target filename pairs # line 1078 + if len({st[1] for st in matches}) != len(matches): # line 1079 + Exit("Some target filenames are not unique and different move/rename actions would point to the same target file") # line 1079 + matches = reorderRenameActions(matches, exitOnConflict=not soft) # attempts to find conflict-free renaming order, or exits # line 1080 + if os.path.exists(encode(newRelPath)): # line 1081 + exists = [filename[1] for filename in matches if os.path.exists(encode(os.path.join(newRelPath, filename[1]).replace(SLASH, os.sep)))] # type: _coconut.typing.Sequence[str] # line 1082 + if exists and not (force or soft): # line 1083 + Exit("%s files would write over existing files in %s cases. Use --force to execute it anyway" % ("Moving" if relPath != newRelPath else "Renaming", "all" if len(exists) == len(matches) else "some")) # line 1083 + else: # line 1084 + os.makedirs(encode(os.path.abspath(newRelPath.replace(SLASH, os.sep)))) # line 1084 + if not soft: # perform actual renaming # line 1085 + for (source, target) in matches: # line 1086 + try: # line 1087 + shutil.move(encode(os.path.abspath(os.path.join(relPath, source).replace(SLASH, os.sep))), encode(os.path.abspath(os.path.join(newRelPath, target).replace(SLASH, os.sep)))) # line 1087 + except Exception as E: # one error can lead to another in case of delicate renaming order # line 1088 + error("Cannot move/rename file '%s' to '%s'" % (source, os.path.join(newRelPath, target))) # one error can lead to another in case of delicate renaming order # line 1088 + patterns[patterns.index(pattern)] = newPattern # line 1089 + m.saveBranches() # line 1090 + +def parse(vcs: 'str', cwd: 'str', cmd: 'str'): # line 1092 + ''' Main operation. root is underlying VCS base dir. main() has already chdir'ed into SOS root folder, cwd is original working directory for add, rm, mv. ''' # line 1093 + debug("Parsing command-line arguments...") # line 1094 + root = os.getcwd() # line 1095 + try: # line 1096 + onlys, excps = parseOnlyOptions(cwd, sys.argv) # extracts folder-relative paths (used in changes, commit, diff, switch, update) # line 1097 + command = sys.argv[1].strip() if len(sys.argv) > 1 else "" # line 1098 + arguments = [c.strip() for c in sys.argv[2:] if not (c.startswith("-") and (len(c) == 2 or c[1] == "-"))] # type: List[_coconut.typing.Optional[str]] # line 1099 + options = [c.strip() for c in sys.argv[2:] if c.startswith("-") and (len(c) == 2 or c[1] == "-")] # options with arguments have to be parsed from sys.argv # line 1100 + debug("Processing command %r with arguments %r and options %r." % (command, [_ for _ in arguments if _ is not None], options)) # line 1101 + if command[:1] in "amr": # line 1102 + relPath, pattern = relativize(root, os.path.join(cwd, arguments[0] if arguments else ".")) # line 1102 + if command[:1] == "m": # line 1103 + if len(arguments) < 2: # line 1104 + Exit("Need a second file pattern argument as target for move command") # line 1104 + newRelPath, newPattern = relativize(root, os.path.join(cwd, arguments[1])) # line 1105 + arguments[:] = (arguments + [None] * 3)[:3] # line 1106 + if command[:1] == "a": # addnot # line 1107 + add(relPath, pattern, options, negative="n" in command) # addnot # line 1107 + elif command[:1] == "b": # line 1108 + branch(arguments[0], arguments[1], options) # line 1108 + elif command[:3] == "com": # line 1109 + commit(arguments[0], options, onlys, excps) # line 1109 + elif command[:2] == "ch": # "changes" (legacy) # line 1110 + changes(arguments[0], options, onlys, excps) # "changes" (legacy) # line 1110 + elif command[:2] == "ci": # line 1111 + commit(arguments[0], options, onlys, excps) # line 1111 + elif command[:3] == 'con': # line 1112 + config(arguments, options) # line 1112 + elif command[:2] == "de": # line 1113 + destroy(arguments[0], options) # line 1113 + elif command[:2] == "di": # line 1114 + diff(arguments[0], options, onlys, excps) # line 1114 + elif command[:2] == "du": # line 1115 + dump(arguments[0], options) # line 1115 + elif command[:1] == "h": # line 1116 + usage.usage(arguments[0], verbose=verbose) # line 1116 + elif command[:2] == "lo": # line 1117 + log(options) # line 1117 + elif command[:2] == "li": # line 1118 + ls(os.path.relpath((lambda _coconut_none_coalesce_item: cwd if _coconut_none_coalesce_item is None else _coconut_none_coalesce_item)(arguments[0]), root), options) # line 1118 + elif command[:2] == "ls": # line 1119 ls(os.path.relpath((lambda _coconut_none_coalesce_item: cwd if _coconut_none_coalesce_item is None else _coconut_none_coalesce_item)(arguments[0]), root), options) # line 1119 - elif command[:2] == "ls": # line 1120 - ls(os.path.relpath((lambda _coconut_none_coalesce_item: cwd if _coconut_none_coalesce_item is None else _coconut_none_coalesce_item)(arguments[0]), root), options) # line 1120 - elif command[:1] == "m": # mvnot # line 1121 - move(relPath, pattern, newRelPath, newPattern, options, negative="n" in command) # mvnot # line 1121 - elif command[:2] == "of": # line 1122 - offline(arguments[0], arguments[1], options) # line 1122 - elif command[:2] == "on": # line 1123 - online(options) # line 1123 - elif command[:1] == "p": # line 1124 - publish(arguments[0], cmd, options, onlys, excps) # line 1124 - elif command[:1] == "r": # rmnot # line 1125 - remove(relPath, pattern, negative="n" in command) # rmnot # line 1125 - elif command[:2] == "st": # line 1126 - status(arguments[0], vcs, cmd, options, onlys, excps) # line 1126 - elif command[:2] == "sw": # line 1127 - switch(arguments[0], options, onlys, excps) # line 1127 - elif command[:1] == "u": # line 1128 - update(arguments[0], options, onlys, excps) # line 1128 - elif command[:1] == "v": # line 1129 - usage.usage(arguments[0], version=True) # line 1129 - else: # line 1130 - Exit("Unknown command '%s'" % command) # line 1130 - Exit(code=0) # regular exit # line 1131 - except (Exception, RuntimeError) as E: # line 1132 - exception(E) # line 1133 - Exit("An internal error occurred in SOS. Please report above message to the project maintainer at https://github.com/ArneBachmann/sos/issues via 'New Issue'.\nPlease state your installed version via 'sos version', and what you were doing") # line 1134 - -def main(): # line 1136 - global debug, info, warn, error # to modify logger # line 1137 - logging.basicConfig(level=level, stream=sys.stderr, format=("%(asctime)-23s %(levelname)-8s %(name)s:%(lineno)d | %(message)s" if '--log' in sys.argv else "%(message)s")) # line 1138 - _log = Logger(logging.getLogger(__name__)) # line 1139 - debug, info, warn, error = _log.debug, _log.info, _log.warn, _log.error # line 1139 - for option in (o for o in ['--log', '--debug', '--verbose', '-v', '--sos', '--vcs'] if o in sys.argv): # clean up program arguments # line 1140 - sys.argv.remove(option) # clean up program arguments # line 1140 - if '--help' in sys.argv or len(sys.argv) < 2: # line 1141 - usage.usage(sys.argv[sys.argv.index('--help') + 1] if '--help' in sys.argv and len(sys.argv) > sys.argv.index('--help') + 1 else None, verbose=verbose) # line 1141 - command = sys.argv[1] if len(sys.argv) > 1 else None # type: _coconut.typing.Optional[str] # line 1142 - root, vcs, cmd = findSosVcsBase() # root is None if no .sos folder exists up the folder tree (still working online); vcs is checkout/repo root folder; cmd is the VCS base command # line 1143 - debug("Detected SOS root folder: %s\nDetected VCS root folder: %s" % (("-" if root is None else root), ("-" if vcs is None else vcs))) # line 1144 - defaults["defaultbranch"] = (lambda _coconut_none_coalesce_item: "default" if _coconut_none_coalesce_item is None else _coconut_none_coalesce_item)(vcsBranches.get(cmd, vcsBranches[SVN])) # sets dynamic default with SVN fallback # line 1145 - defaults["useChangesCommand"] = cmd == "fossil" # sets dynamic default with SVN fallback # line 1146 - if force_sos or root is not None or (("" if command is None else command))[:2] == "of" or (("" if command is None else command))[:1] in "hv": # in offline mode or just going offline TODO what about git config? # line 1147 - cwd = os.getcwd() # line 1148 - os.chdir(cwd if command[:2] == "of" else (cwd if root is None else root)) # line 1149 - parse(vcs, cwd, cmd) # line 1150 - elif force_vcs or cmd is not None: # online mode - delegate to VCS # line 1151 - info("%s: Running '%s %s'" % (usage.COMMAND.upper(), cmd, " ".join(sys.argv[1:]))) # line 1152 - import subprocess # only required in this section # line 1153 - process = subprocess.Popen([cmd] + sys.argv[1:], shell=False, stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr) # line 1154 - inp = "" # type: str # line 1155 - while True: # line 1156 - so, se = process.communicate(input=inp) # line 1157 - if process.returncode is not None: # line 1158 - break # line 1158 - inp = sys.stdin.read() # line 1159 - if sys.argv[1][:2] == "co" and process.returncode == 0: # successful commit - assume now in sync again (but leave meta data folder with potential other feature branches behind until "online") # line 1160 - if root is None: # line 1161 - Exit("Cannot determine VCS root folder: Unable to mark repository as synchronized and will show a warning when leaving offline mode") # line 1161 - m = Metadata(root) # type: Metadata # line 1162 - m.branches[m.branch] = dataCopy(BranchInfo, m.branches[m.branch], inSync=True) # mark as committed # line 1163 - m.saveBranches() # line 1164 - else: # line 1165 - Exit("No offline repository present, and unable to detect VCS file tree") # line 1165 + elif command[:1] == "m": # mvnot # line 1120 + move(relPath, pattern, newRelPath, newPattern, options, negative="n" in command) # mvnot # line 1120 + elif command[:2] == "of": # line 1121 + offline(arguments[0], arguments[1], options) # line 1121 + elif command[:2] == "on": # line 1122 + online(options) # line 1122 + elif command[:1] == "p": # line 1123 + publish(arguments[0], cmd, options, onlys, excps) # line 1123 + elif command[:1] == "r": # rmnot # line 1124 + remove(relPath, pattern, negative="n" in command) # rmnot # line 1124 + elif command[:2] == "st": # line 1125 + status(arguments[0], vcs, cmd, options, onlys, excps) # line 1125 + elif command[:2] == "sw": # line 1126 + switch(arguments[0], options, onlys, excps) # line 1126 + elif command[:1] == "u": # line 1127 + update(arguments[0], options, onlys, excps) # line 1127 + elif command[:1] == "v": # line 1128 + usage.usage(arguments[0], version=True) # line 1128 + else: # line 1129 + Exit("Unknown command '%s'" % command) # line 1129 + Exit(code=0) # regular exit # line 1130 + except (Exception, RuntimeError) as E: # line 1131 + exception(E) # line 1132 + Exit("An internal error occurred in SOS. Please report above message to the project maintainer at https://github.com/ArneBachmann/sos/issues via 'New Issue'.\nPlease state your installed version via 'sos version', and what you were doing") # line 1133 + +def main(): # line 1135 + global debug, info, warn, error # to modify logger # line 1136 + logging.basicConfig(level=level, stream=sys.stderr, format=("%(asctime)-23s %(levelname)-8s %(name)s:%(lineno)d | %(message)s" if '--log' in sys.argv else "%(message)s")) # line 1137 + _log = Logger(logging.getLogger(__name__)) # line 1138 + debug, info, warn, error = _log.debug, _log.info, _log.warn, _log.error # line 1138 + for option in (o for o in ['--log', '--debug', '--verbose', '-v', '--sos', '--vcs'] if o in sys.argv): # clean up program arguments # line 1139 + sys.argv.remove(option) # clean up program arguments # line 1139 + if '--help' in sys.argv or len(sys.argv) < 2: # line 1140 + usage.usage(sys.argv[sys.argv.index('--help') + 1] if '--help' in sys.argv and len(sys.argv) > sys.argv.index('--help') + 1 else None, verbose=verbose) # line 1140 + command = sys.argv[1] if len(sys.argv) > 1 else None # type: _coconut.typing.Optional[str] # line 1141 + root, vcs, cmd = findSosVcsBase() # root is None if no .sos folder exists up the folder tree (still working online); vcs is checkout/repo root folder; cmd is the VCS base command # line 1142 + debug("Detected SOS root folder: %s\nDetected VCS root folder: %s" % (("-" if root is None else root), ("-" if vcs is None else vcs))) # line 1143 + defaults["defaultbranch"] = (lambda _coconut_none_coalesce_item: "default" if _coconut_none_coalesce_item is None else _coconut_none_coalesce_item)(vcsBranches.get(cmd, vcsBranches[SVN])) # sets dynamic default with SVN fallback # line 1144 + defaults["useChangesCommand"] = cmd == "fossil" # sets dynamic default with SVN fallback # line 1145 + if (not force_vcs or force_sos) and (root is not None or (("" if command is None else command))[:2] == "of" or (("_" if command is None else command))[:1] in "hv"): # in offline mode or just going offline TODO what about git config? # line 1146 + cwd = os.getcwd() # line 1147 + os.chdir(cwd if command[:2] == "of" else (cwd if root is None else root)) # line 1148 + parse(vcs, cwd, cmd) # line 1149 + elif force_vcs or cmd is not None: # online mode - delegate to VCS # line 1150 + info("%s: Running '%s %s'" % (usage.COMMAND.upper(), cmd, " ".join(sys.argv[1:]))) # line 1151 + import subprocess # only required in this section # line 1152 + process = subprocess.Popen([cmd] + sys.argv[1:], shell=False, stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr) # line 1153 + inp = "" # type: str # line 1154 + while True: # line 1155 + so, se = process.communicate(input=inp) # line 1156 + if process.returncode is not None: # line 1157 + break # line 1157 + inp = sys.stdin.read() # line 1158 + if sys.argv[1][:2] == "co" and process.returncode == 0: # successful commit - assume now in sync again (but leave meta data folder with potential other feature branches behind until "online") # line 1159 + if root is None: # line 1160 + Exit("Cannot determine VCS root folder: Unable to mark repository as synchronized and will show a warning when leaving offline mode") # line 1160 + m = Metadata(root) # type: Metadata # line 1161 + m.branches[m.branch] = dataCopy(BranchInfo, m.branches[m.branch], inSync=True) # mark as committed # line 1162 + m.saveBranches() # line 1163 + else: # line 1164 + Exit("No offline repository present, and unable to detect VCS file tree") # line 1164 # Main part -force_sos = '--sos' in sys.argv # type: bool # line 1169 -force_vcs = '--vcs' in sys.argv # type: bool # line 1170 -verbose = '--verbose' in sys.argv or '-v' in sys.argv # type: bool # imported from utility, and only modified here # line 1171 -debug_ = os.environ.get("DEBUG", "False").lower() == "true" or '--debug' in sys.argv # type: bool # line 1172 -level = logging.DEBUG if '--debug' in sys.argv else logging.INFO # type: int # line 1173 -_log = Logger(logging.getLogger(__name__)) # line 1174 -debug, info, warn, error = _log.debug, _log.info, _log.warn, _log.error # line 1174 -if __name__ == '__main__': # line 1175 - main() # line 1175 +force_sos = '--sos' in sys.argv # type: bool # line 1168 +force_vcs = '--vcs' in sys.argv # type: bool # line 1169 +verbose = '--verbose' in sys.argv or '-v' in sys.argv # type: bool # imported from utility, and only modified here # line 1170 +debug_ = os.environ.get("DEBUG", "False").lower() == "true" or '--debug' in sys.argv # type: bool # line 1171 +level = logging.DEBUG if '--debug' in sys.argv else logging.INFO # type: int # line 1172 +_log = Logger(logging.getLogger(__name__)) # line 1173 +debug, info, warn, error = _log.debug, _log.info, _log.warn, _log.error # line 1173 +if __name__ == '__main__': # line 1174 + main() # line 1174 diff --git a/sos/tests.coco b/sos/tests.coco index 3a7cee7..766741f 100644 --- a/sos/tests.coco +++ b/sos/tests.coco @@ -880,6 +880,8 @@ class Tests(unittest.TestCase): except SystemExit as E: _.assertEqual(0, E.code) out = wrapChannels(-> sos.config(["show"])).replace("\r", "") _.assertIn(" ignores [global] ['ign1', 'ign2']", out) + out = wrapChannels(-> sos.config(["show", "ignores"])).replace("\r", "") + _.assertIn(" ignores [global] ['ign1', 'ign2']", out) out = sos.safeSplit(wrapChannels(-> sos.ls()).replace("\r", ""), "\n") _.assertInAny(' file1', out) _.assertInAny(' ign1', out) @@ -1008,7 +1010,7 @@ class Tests(unittest.TestCase): _.createFile(3) out = wrapChannels(-> sos.diff("/-2")) # compare with r1 (second counting from last which is r2) _.assertIn("ADD ./file3", out) - _.assertAllIn(["MOD ./file2", "DIF ./file1 ", "- | 0 |xxxxxxxxxx|", "+ | 0 |foobar|"], out) + _.assertAllIn(["MOD ./file2", "DIF ./file1 ", "-~- 0 |xxxxxxxxxx|", "+~+ 0 |foobar|"], out) _.assertAllNotIn(["MOD ./file1", "DIF ./file1"], wrapChannels(-> sos.diff("/-2", onlys = f{"./file2"}))) def testReorderRenameActions(_): diff --git a/sos/tests.py b/sos/tests.py index 89d1a8f..da67308 100644 --- a/sos/tests.py +++ b/sos/tests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# __coconut_hash__ = 0x737f5b0d +# __coconut_hash__ = 0xae15dafd # Compiled with Coconut version 1.3.1-post_dev28 [Dead Parrot] @@ -1031,105 +1031,102 @@ def testLsSimple(_): # line 868 _.assertEqual(0, E.code) # line 880 out = wrapChannels(lambda _=None: sos.config(["show"])).replace("\r", "") # line 881 _.assertIn(" ignores [global] ['ign1', 'ign2']", out) # line 882 - out = sos.safeSplit(wrapChannels(lambda _=None: sos.ls()).replace("\r", ""), "\n") # line 883 - _.assertInAny(' file1', out) # line 884 - _.assertInAny(' ign1', out) # line 885 - _.assertInAny(' ign2', out) # line 886 - _.assertNotIn('DIR sub', out) # line 887 - _.assertNotIn(' bar', out) # line 888 - out = wrapChannels(lambda _=None: sos.ls(options=["--recursive"])).replace("\r", "") # line 889 - _.assertIn('DIR sub', out) # line 890 - _.assertIn(' bar', out) # line 891 - try: # line 892 - sos.config(["rm", "foo", "bar"]) # line 892 - _.fail() # line 892 - except SystemExit as E: # line 893 - _.assertEqual(1, E.code) # line 893 + out = wrapChannels(lambda _=None: sos.config(["show", "ignores"])).replace("\r", "") # line 883 + _.assertIn(" ignores [global] ['ign1', 'ign2']", out) # line 884 + out = sos.safeSplit(wrapChannels(lambda _=None: sos.ls()).replace("\r", ""), "\n") # line 885 + _.assertInAny(' file1', out) # line 886 + _.assertInAny(' ign1', out) # line 887 + _.assertInAny(' ign2', out) # line 888 + _.assertNotIn('DIR sub', out) # line 889 + _.assertNotIn(' bar', out) # line 890 + out = wrapChannels(lambda _=None: sos.ls(options=["--recursive"])).replace("\r", "") # line 891 + _.assertIn('DIR sub', out) # line 892 + _.assertIn(' bar', out) # line 893 try: # line 894 - sos.config(["rm", "ignores", "foo"]) # line 894 + sos.config(["rm", "foo", "bar"]) # line 894 _.fail() # line 894 except SystemExit as E: # line 895 _.assertEqual(1, E.code) # line 895 try: # line 896 - sos.config(["rm", "ignores", "ign1"]) # line 896 + sos.config(["rm", "ignores", "foo"]) # line 896 + _.fail() # line 896 except SystemExit as E: # line 897 - _.assertEqual(0, E.code) # line 897 - try: # remove ignore pattern # line 898 - sos.config(["unset", "ignoresWhitelist"]) # remove ignore pattern # line 898 + _.assertEqual(1, E.code) # line 897 + try: # line 898 + sos.config(["rm", "ignores", "ign1"]) # line 898 except SystemExit as E: # line 899 _.assertEqual(0, E.code) # line 899 - out = sos.safeSplit(wrapChannels(lambda _=None: sos.ls()).replace("\r", ""), "\n") # line 900 - _.assertInAny(' ign1', out) # line 901 - _.assertInAny('IGN ign2', out) # line 902 - _.assertNotInAny(' ign2', out) # line 903 - - def testWhitelist(_): # line 905 + try: # remove ignore pattern # line 900 + sos.config(["unset", "ignoresWhitelist"]) # remove ignore pattern # line 900 + except SystemExit as E: # line 901 + _.assertEqual(0, E.code) # line 901 + out = sos.safeSplit(wrapChannels(lambda _=None: sos.ls()).replace("\r", ""), "\n") # line 902 + _.assertInAny(' ign1', out) # line 903 + _.assertInAny('IGN ign2', out) # line 904 + _.assertNotInAny(' ign2', out) # line 905 + + def testWhitelist(_): # line 907 # TODO test same for simple mode - _.createFile(1) # line 907 - sos.defaults.ignores[:] = ["file*"] # replace in-place # line 908 - sos.offline("xx", options=["--track", "--strict"]) # because nothing to commit due to ignore pattern # line 909 - sos.add(".", "./file*") # add tracking pattern for "file1" # line 910 - sos.commit(options=["--force"]) # attempt to commit the file # line 911 - _.assertEqual(1, len(os.listdir(sos.revisionFolder(0, 1)))) # only meta data, file1 was ignored # line 912 - try: # Exit because dirty # line 913 - sos.online() # Exit because dirty # line 913 - _.fail() # Exit because dirty # line 913 - except: # exception expected # line 914 - pass # exception expected # line 914 - _.createFile("x2") # add another change # line 915 - sos.add(".", "./x?") # add tracking pattern for "file1" # line 916 - try: # force beyond dirty flag check # line 917 - sos.online(["--force"]) # force beyond dirty flag check # line 917 - _.fail() # force beyond dirty flag check # line 917 - except: # line 918 - pass # line 918 - sos.online(["--force", "--force"]) # force beyond file tree modifications check # line 919 - _.assertFalse(os.path.exists(sos.metaFolder)) # line 920 - - _.createFile(1) # line 922 - sos.defaults.ignoresWhitelist[:] = ["file*"] # line 923 - sos.offline("xx", None, ["--track"]) # line 924 - sos.add(".", "./file*") # line 925 - sos.commit() # should NOT ask for force here # line 926 - _.assertEqual(2, len(os.listdir(sos.revisionFolder(0, 1)))) # meta data and "file1", file1 was whitelisted # line 927 - - def testRemove(_): # line 929 - _.createFile(1, "x" * 100) # line 930 - sos.offline("trunk") # line 931 - try: # line 932 - sos.destroy("trunk") # line 932 - _fail() # line 932 - except: # line 933 - pass # line 933 - _.createFile(2, "y" * 10) # line 934 - sos.branch("added") # creates new branch, writes repo metadata, and therefore creates backup copy # line 935 - sos.destroy("trunk") # line 936 - _.assertAllIn([sos.metaFile, sos.metaBack, "b0_last", "b1"], os.listdir("." + os.sep + sos.metaFolder)) # line 937 - _.assertTrue(os.path.exists("." + os.sep + sos.metaFolder + os.sep + "b1")) # line 938 - _.assertFalse(os.path.exists("." + os.sep + sos.metaFolder + os.sep + "b0")) # line 939 - sos.branch("next") # line 940 - _.createFile(3, "y" * 10) # make a change # line 941 - sos.destroy("added", "--force") # should succeed # line 942 - - def testUsage(_): # line 944 - try: # TODO expect sys.exit(0) # line 945 - sos.usage() # TODO expect sys.exit(0) # line 945 - _.fail() # TODO expect sys.exit(0) # line 945 - except: # line 946 - pass # line 946 + _.createFile(1) # line 909 + sos.defaults.ignores[:] = ["file*"] # replace in-place # line 910 + sos.offline("xx", options=["--track", "--strict"]) # because nothing to commit due to ignore pattern # line 911 + sos.add(".", "./file*") # add tracking pattern for "file1" # line 912 + sos.commit(options=["--force"]) # attempt to commit the file # line 913 + _.assertEqual(1, len(os.listdir(sos.revisionFolder(0, 1)))) # only meta data, file1 was ignored # line 914 + try: # Exit because dirty # line 915 + sos.online() # Exit because dirty # line 915 + _.fail() # Exit because dirty # line 915 + except: # exception expected # line 916 + pass # exception expected # line 916 + _.createFile("x2") # add another change # line 917 + sos.add(".", "./x?") # add tracking pattern for "file1" # line 918 + try: # force beyond dirty flag check # line 919 + sos.online(["--force"]) # force beyond dirty flag check # line 919 + _.fail() # force beyond dirty flag check # line 919 + except: # line 920 + pass # line 920 + sos.online(["--force", "--force"]) # force beyond file tree modifications check # line 921 + _.assertFalse(os.path.exists(sos.metaFolder)) # line 922 + + _.createFile(1) # line 924 + sos.defaults.ignoresWhitelist[:] = ["file*"] # line 925 + sos.offline("xx", None, ["--track"]) # line 926 + sos.add(".", "./file*") # line 927 + sos.commit() # should NOT ask for force here # line 928 + _.assertEqual(2, len(os.listdir(sos.revisionFolder(0, 1)))) # meta data and "file1", file1 was whitelisted # line 929 + + def testRemove(_): # line 931 + _.createFile(1, "x" * 100) # line 932 + sos.offline("trunk") # line 933 + try: # line 934 + sos.destroy("trunk") # line 934 + _fail() # line 934 + except: # line 935 + pass # line 935 + _.createFile(2, "y" * 10) # line 936 + sos.branch("added") # creates new branch, writes repo metadata, and therefore creates backup copy # line 937 + sos.destroy("trunk") # line 938 + _.assertAllIn([sos.metaFile, sos.metaBack, "b0_last", "b1"], os.listdir("." + os.sep + sos.metaFolder)) # line 939 + _.assertTrue(os.path.exists("." + os.sep + sos.metaFolder + os.sep + "b1")) # line 940 + _.assertFalse(os.path.exists("." + os.sep + sos.metaFolder + os.sep + "b0")) # line 941 + sos.branch("next") # line 942 + _.createFile(3, "y" * 10) # make a change # line 943 + sos.destroy("added", "--force") # should succeed # line 944 + + def testUsage(_): # line 946 try: # TODO expect sys.exit(0) # line 947 - sos.usage("help") # TODO expect sys.exit(0) # line 947 + sos.usage() # TODO expect sys.exit(0) # line 947 _.fail() # TODO expect sys.exit(0) # line 947 except: # line 948 pass # line 948 try: # TODO expect sys.exit(0) # line 949 - sos.usage("help", verbose=True) # TODO expect sys.exit(0) # line 949 + sos.usage("help") # TODO expect sys.exit(0) # line 949 _.fail() # TODO expect sys.exit(0) # line 949 except: # line 950 pass # line 950 - try: # line 951 - sos.usage(version=True) # line 951 - _.fail() # line 951 + try: # TODO expect sys.exit(0) # line 951 + sos.usage("help", verbose=True) # TODO expect sys.exit(0) # line 951 + _.fail() # TODO expect sys.exit(0) # line 951 except: # line 952 pass # line 952 try: # line 953 @@ -1137,195 +1134,200 @@ def testUsage(_): # line 944 _.fail() # line 953 except: # line 954 pass # line 954 - - def testOnlyExcept(_): # line 956 - ''' Test blacklist glob rules. ''' # line 957 - sos.offline(options=["--track"]) # line 958 - _.createFile("a.1") # line 959 - _.createFile("a.2") # line 960 - _.createFile("b.1") # line 961 - _.createFile("b.2") # line 962 - sos.add(".", "./a.?") # line 963 - sos.add(".", "./?.1", negative=True) # line 964 - out = wrapChannels(lambda _=None: sos.commit()) # line 965 - _.assertIn("ADD ./a.2", out) # line 966 - _.assertNotIn("ADD ./a.1", out) # line 967 - _.assertNotIn("ADD ./b.1", out) # line 968 - _.assertNotIn("ADD ./b.2", out) # line 969 - - def testOnly(_): # line 971 - _.assertEqual((_coconut.frozenset(("./A", "x/B")), _coconut.frozenset(("./C",))), sos.parseOnlyOptions(".", ["abc", "def", "--only", "A", "--x", "--only", "x/B", "--except", "C", "--only"])) # line 972 - _.assertEqual(_coconut.frozenset(("B",)), sos.conditionalIntersection(_coconut.frozenset(("A", "B", "C")), _coconut.frozenset(("B", "D")))) # line 973 - _.assertEqual(_coconut.frozenset(("B", "D")), sos.conditionalIntersection(_coconut.frozenset(), _coconut.frozenset(("B", "D")))) # line 974 - _.assertEqual(_coconut.frozenset(("B", "D")), sos.conditionalIntersection(None, _coconut.frozenset(("B", "D")))) # line 975 - sos.offline(options=["--track", "--strict"]) # line 976 - _.createFile(1) # line 977 - _.createFile(2) # line 978 - sos.add(".", "./file1") # line 979 - sos.add(".", "./file2") # line 980 - sos.commit(onlys=_coconut.frozenset(("./file1",))) # line 981 - _.assertEqual(2, len(os.listdir(sos.revisionFolder(0, 1)))) # only meta and file1 # line 982 - sos.commit() # adds also file2 # line 983 - _.assertEqual(2, len(os.listdir(sos.revisionFolder(0, 2)))) # only meta and file1 # line 984 - _.createFile(1, "cc") # modify both files # line 985 - _.createFile(2, "dd") # line 986 - try: # line 987 - sos.config(["set", "texttype", "file2"]) # line 987 - except SystemExit as E: # line 988 - _.assertEqual(0, E.code) # line 988 - changes = sos.changes(excps=_coconut.frozenset(("./file1",))) # line 989 - _.assertEqual(1, len(changes.modifications)) # only file2 # line 990 - _.assertTrue("./file2" in changes.modifications) # line 991 - _.assertAllIn(["DIF ./file2", ""], wrapChannels(lambda _=None: sos.diff(onlys=_coconut.frozenset(("./file2",))))) # line 992 - _.assertAllNotIn(["MOD ./file1", "DIF ./file1", "MOD ./file2"], wrapChannels(lambda _=None: sos.diff(onlys=_coconut.frozenset(("./file2",))))) # MOD vs. DIF # line 993 - _.assertIn("MOD ./file1", wrapChannels(lambda _=None: sos.diff(excps=_coconut.frozenset(("./file2",))))) # MOD vs. DIF # line 994 - _.assertNotIn("MOD ./file2", wrapChannels(lambda _=None: sos.diff(excps=_coconut.frozenset(("./file2",))))) # line 995 - - def testDiff(_): # line 997 - try: # manually mark this file as "textual" # line 998 - sos.config(["set", "texttype", "file1"]) # manually mark this file as "textual" # line 998 - except SystemExit as E: # line 999 - _.assertEqual(0, E.code) # line 999 - sos.offline(options=["--strict"]) # line 1000 - _.createFile(1) # line 1001 - _.createFile(2) # line 1002 - sos.commit() # line 1003 - _.createFile(1, "sdfsdgfsdf") # line 1004 - _.createFile(2, "12343") # line 1005 - sos.commit() # line 1006 - _.createFile(1, "foobar") # line 1007 - _.createFile(3) # line 1008 - out = wrapChannels(lambda _=None: sos.diff("/-2")) # compare with r1 (second counting from last which is r2) # line 1009 - _.assertIn("ADD ./file3", out) # line 1010 - _.assertAllIn(["MOD ./file2", "DIF ./file1 ", "- | 0 |xxxxxxxxxx|", "+ | 0 |foobar|"], out) # line 1011 - _.assertAllNotIn(["MOD ./file1", "DIF ./file1"], wrapChannels(lambda _=None: sos.diff("/-2", onlys=_coconut.frozenset(("./file2",))))) # line 1012 - - def testReorderRenameActions(_): # line 1014 - result = sos.reorderRenameActions([("123", "312"), ("312", "132"), ("321", "123")], exitOnConflict=False) # type: Tuple[str, str] # line 1015 - _.assertEqual([("312", "132"), ("123", "312"), ("321", "123")], result) # line 1016 - try: # line 1017 - sos.reorderRenameActions([("123", "312"), ("312", "123")], exitOnConflict=True) # line 1017 - _.fail() # line 1017 - except: # line 1018 - pass # line 1018 - - def testPublish(_): # line 1020 - pass # TODO how to test without modifying anything underlying? probably use --test flag or similar? # line 1021 - - def testMove(_): # line 1023 - sos.offline(options=["--strict", "--track"]) # line 1024 - _.createFile(1) # line 1025 - sos.add(".", "./file?") # line 1026 + try: # line 955 + sos.usage(version=True) # line 955 + _.fail() # line 955 + except: # line 956 + pass # line 956 + + def testOnlyExcept(_): # line 958 + ''' Test blacklist glob rules. ''' # line 959 + sos.offline(options=["--track"]) # line 960 + _.createFile("a.1") # line 961 + _.createFile("a.2") # line 962 + _.createFile("b.1") # line 963 + _.createFile("b.2") # line 964 + sos.add(".", "./a.?") # line 965 + sos.add(".", "./?.1", negative=True) # line 966 + out = wrapChannels(lambda _=None: sos.commit()) # line 967 + _.assertIn("ADD ./a.2", out) # line 968 + _.assertNotIn("ADD ./a.1", out) # line 969 + _.assertNotIn("ADD ./b.1", out) # line 970 + _.assertNotIn("ADD ./b.2", out) # line 971 + + def testOnly(_): # line 973 + _.assertEqual((_coconut.frozenset(("./A", "x/B")), _coconut.frozenset(("./C",))), sos.parseOnlyOptions(".", ["abc", "def", "--only", "A", "--x", "--only", "x/B", "--except", "C", "--only"])) # line 974 + _.assertEqual(_coconut.frozenset(("B",)), sos.conditionalIntersection(_coconut.frozenset(("A", "B", "C")), _coconut.frozenset(("B", "D")))) # line 975 + _.assertEqual(_coconut.frozenset(("B", "D")), sos.conditionalIntersection(_coconut.frozenset(), _coconut.frozenset(("B", "D")))) # line 976 + _.assertEqual(_coconut.frozenset(("B", "D")), sos.conditionalIntersection(None, _coconut.frozenset(("B", "D")))) # line 977 + sos.offline(options=["--track", "--strict"]) # line 978 + _.createFile(1) # line 979 + _.createFile(2) # line 980 + sos.add(".", "./file1") # line 981 + sos.add(".", "./file2") # line 982 + sos.commit(onlys=_coconut.frozenset(("./file1",))) # line 983 + _.assertEqual(2, len(os.listdir(sos.revisionFolder(0, 1)))) # only meta and file1 # line 984 + sos.commit() # adds also file2 # line 985 + _.assertEqual(2, len(os.listdir(sos.revisionFolder(0, 2)))) # only meta and file1 # line 986 + _.createFile(1, "cc") # modify both files # line 987 + _.createFile(2, "dd") # line 988 + try: # line 989 + sos.config(["set", "texttype", "file2"]) # line 989 + except SystemExit as E: # line 990 + _.assertEqual(0, E.code) # line 990 + changes = sos.changes(excps=_coconut.frozenset(("./file1",))) # line 991 + _.assertEqual(1, len(changes.modifications)) # only file2 # line 992 + _.assertTrue("./file2" in changes.modifications) # line 993 + _.assertAllIn(["DIF ./file2", ""], wrapChannels(lambda _=None: sos.diff(onlys=_coconut.frozenset(("./file2",))))) # line 994 + _.assertAllNotIn(["MOD ./file1", "DIF ./file1", "MOD ./file2"], wrapChannels(lambda _=None: sos.diff(onlys=_coconut.frozenset(("./file2",))))) # MOD vs. DIF # line 995 + _.assertIn("MOD ./file1", wrapChannels(lambda _=None: sos.diff(excps=_coconut.frozenset(("./file2",))))) # MOD vs. DIF # line 996 + _.assertNotIn("MOD ./file2", wrapChannels(lambda _=None: sos.diff(excps=_coconut.frozenset(("./file2",))))) # line 997 + + def testDiff(_): # line 999 + try: # manually mark this file as "textual" # line 1000 + sos.config(["set", "texttype", "file1"]) # manually mark this file as "textual" # line 1000 + except SystemExit as E: # line 1001 + _.assertEqual(0, E.code) # line 1001 + sos.offline(options=["--strict"]) # line 1002 + _.createFile(1) # line 1003 + _.createFile(2) # line 1004 + sos.commit() # line 1005 + _.createFile(1, "sdfsdgfsdf") # line 1006 + _.createFile(2, "12343") # line 1007 + sos.commit() # line 1008 + _.createFile(1, "foobar") # line 1009 + _.createFile(3) # line 1010 + out = wrapChannels(lambda _=None: sos.diff("/-2")) # compare with r1 (second counting from last which is r2) # line 1011 + _.assertIn("ADD ./file3", out) # line 1012 + _.assertAllIn(["MOD ./file2", "DIF ./file1 ", "-~- 0 |xxxxxxxxxx|", "+~+ 0 |foobar|"], out) # line 1013 + _.assertAllNotIn(["MOD ./file1", "DIF ./file1"], wrapChannels(lambda _=None: sos.diff("/-2", onlys=_coconut.frozenset(("./file2",))))) # line 1014 + + def testReorderRenameActions(_): # line 1016 + result = sos.reorderRenameActions([("123", "312"), ("312", "132"), ("321", "123")], exitOnConflict=False) # type: Tuple[str, str] # line 1017 + _.assertEqual([("312", "132"), ("123", "312"), ("321", "123")], result) # line 1018 + try: # line 1019 + sos.reorderRenameActions([("123", "312"), ("312", "123")], exitOnConflict=True) # line 1019 + _.fail() # line 1019 + except: # line 1020 + pass # line 1020 + + def testPublish(_): # line 1022 + pass # TODO how to test without modifying anything underlying? probably use --test flag or similar? # line 1023 + + def testMove(_): # line 1025 + sos.offline(options=["--strict", "--track"]) # line 1026 + _.createFile(1) # line 1027 + sos.add(".", "./file?") # line 1028 # test source folder missing - try: # line 1028 - sos.move("sub", "sub/file?", ".", "?file") # line 1028 - _.fail() # line 1028 - except: # line 1029 - pass # line 1029 + try: # line 1030 + sos.move("sub", "sub/file?", ".", "?file") # line 1030 + _.fail() # line 1030 + except: # line 1031 + pass # line 1031 # test target folder missing: create it - sos.move(".", "./file?", "sub", "sub/file?") # line 1031 - _.assertTrue(os.path.exists("sub")) # line 1032 - _.assertTrue(os.path.exists("sub/file1")) # line 1033 - _.assertFalse(os.path.exists("file1")) # line 1034 + sos.move(".", "./file?", "sub", "sub/file?") # line 1033 + _.assertTrue(os.path.exists("sub")) # line 1034 + _.assertTrue(os.path.exists("sub/file1")) # line 1035 + _.assertFalse(os.path.exists("file1")) # line 1036 # test move - sos.move("sub", "sub/file?", ".", "./?file") # line 1036 - _.assertTrue(os.path.exists("1file")) # line 1037 - _.assertFalse(os.path.exists("sub/file1")) # line 1038 + sos.move("sub", "sub/file?", ".", "./?file") # line 1038 + _.assertTrue(os.path.exists("1file")) # line 1039 + _.assertFalse(os.path.exists("sub/file1")) # line 1040 # test nothing matches source pattern - try: # line 1040 - sos.move(".", "a*", ".", "b*") # line 1040 - _.fail() # line 1040 - except: # line 1041 - pass # line 1041 - sos.add(".", "*") # anything pattern # line 1042 - try: # TODO check that alternative pattern "*" was suggested (1 hit) # line 1043 - sos.move(".", "a*", ".", "b*") # TODO check that alternative pattern "*" was suggested (1 hit) # line 1043 - _.fail() # TODO check that alternative pattern "*" was suggested (1 hit) # line 1043 - except: # line 1044 - pass # line 1044 + try: # line 1042 + sos.move(".", "a*", ".", "b*") # line 1042 + _.fail() # line 1042 + except: # line 1043 + pass # line 1043 + sos.add(".", "*") # anything pattern # line 1044 + try: # TODO check that alternative pattern "*" was suggested (1 hit) # line 1045 + sos.move(".", "a*", ".", "b*") # TODO check that alternative pattern "*" was suggested (1 hit) # line 1045 + _.fail() # TODO check that alternative pattern "*" was suggested (1 hit) # line 1045 + except: # line 1046 + pass # line 1046 # test rename no conflict - _.createFile(1) # line 1046 - _.createFile(2) # line 1047 - _.createFile(3) # line 1048 - sos.add(".", "./file*") # line 1049 - try: # define an ignore pattern # line 1050 - sos.config(["set", "ignores", "file3;file4"]) # define an ignore pattern # line 1050 - except SystemExit as E: # line 1051 - _.assertEqual(0, E.code) # line 1051 - try: # line 1052 - sos.config(["set", "ignoresWhitelist", "file3"]) # line 1052 + _.createFile(1) # line 1048 + _.createFile(2) # line 1049 + _.createFile(3) # line 1050 + sos.add(".", "./file*") # line 1051 + try: # define an ignore pattern # line 1052 + sos.config(["set", "ignores", "file3;file4"]) # define an ignore pattern # line 1052 except SystemExit as E: # line 1053 _.assertEqual(0, E.code) # line 1053 - sos.move(".", "./file*", ".", "fi*le") # line 1054 - _.assertTrue(all((os.path.exists("fi%dle" % i) for i in range(1, 4)))) # line 1055 - _.assertFalse(os.path.exists("fi4le")) # line 1056 + try: # line 1054 + sos.config(["set", "ignoresWhitelist", "file3"]) # line 1054 + except SystemExit as E: # line 1055 + _.assertEqual(0, E.code) # line 1055 + sos.move(".", "./file*", ".", "fi*le") # line 1056 + _.assertTrue(all((os.path.exists("fi%dle" % i) for i in range(1, 4)))) # line 1057 + _.assertFalse(os.path.exists("fi4le")) # line 1058 # test rename solvable conflicts - [_.createFile("%s-%s-%s" % tuple((c for c in n))) for n in ["312", "321", "123", "231"]] # line 1058 + [_.createFile("%s-%s-%s" % tuple((c for c in n))) for n in ["312", "321", "123", "231"]] # line 1060 # sos.move("?-?-?") # test rename unsolvable conflicts # test --soft option - sos.remove(".", "./?file") # was renamed before # line 1062 - sos.add(".", "./?a?b", ["--force"]) # line 1063 - sos.move(".", "./?a?b", ".", "./a?b?", ["--force", "--soft"]) # line 1064 - _.createFile("1a2b") # should not be tracked # line 1065 - _.createFile("a1b2") # should be tracked # line 1066 - sos.commit() # line 1067 - _.assertEqual(2, len(os.listdir(sos.revisionFolder(0, 1)))) # line 1068 - _.assertTrue(os.path.exists(sos.revisionFolder(0, 1, file="93b38f90892eb5c57779ca9c0b6fbdf6774daeee3342f56f3e78eb2fe5336c50"))) # a1b2 # line 1069 - _.createFile("1a1b1") # line 1070 - _.createFile("1a1b2") # line 1071 - sos.add(".", "?a?b*") # line 1072 - _.assertIn("not unique", wrapChannels(lambda _=None: sos.move(".", "?a?b*", ".", "z?z?"))) # should raise error due to same target name # line 1073 + sos.remove(".", "./?file") # was renamed before # line 1064 + sos.add(".", "./?a?b", ["--force"]) # line 1065 + sos.move(".", "./?a?b", ".", "./a?b?", ["--force", "--soft"]) # line 1066 + _.createFile("1a2b") # should not be tracked # line 1067 + _.createFile("a1b2") # should be tracked # line 1068 + sos.commit() # line 1069 + _.assertEqual(2, len(os.listdir(sos.revisionFolder(0, 1)))) # line 1070 + _.assertTrue(os.path.exists(sos.revisionFolder(0, 1, file="93b38f90892eb5c57779ca9c0b6fbdf6774daeee3342f56f3e78eb2fe5336c50"))) # a1b2 # line 1071 + _.createFile("1a1b1") # line 1072 + _.createFile("1a1b2") # line 1073 + sos.add(".", "?a?b*") # line 1074 + _.assertIn("not unique", wrapChannels(lambda _=None: sos.move(".", "?a?b*", ".", "z?z?"))) # should raise error due to same target name # line 1075 # 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 testAskUpdate(_): # line 1077 - _.createFile(1) # line 1078 - _.createFile(3) # line 1079 - _.createFile(5) # line 1080 - sos.offline() # branch 0: only file1 # line 1081 - sos.branch() # line 1082 - os.unlink("file1") # line 1083 - os.unlink("file3") # line 1084 - os.unlink("file5") # line 1085 - _.createFile(2) # line 1086 - _.createFile(4) # line 1087 - _.createFile(6) # line 1088 - sos.commit() # branch 1: only file2 # line 1089 - sos.switch("0/") # line 1090 - mockInput(["y", "a", "y", "a"], lambda _=None: sos.update("1/", ["--ask"])) # line 1091 - _.assertFalse(_.existsFile(1)) # line 1092 - _.assertFalse(_.existsFile(3)) # line 1093 - _.assertFalse(_.existsFile(5)) # line 1094 - _.assertTrue(_.existsFile(2)) # line 1095 - _.assertTrue(_.existsFile(4)) # line 1096 - _.assertTrue(_.existsFile(6)) # line 1097 - - def testHashCollision(_): # line 1099 - sos.offline() # line 1100 - _.createFile(1) # line 1101 - os.mkdir(sos.revisionFolder(0, 1)) # line 1102 - _.createFile("b9ee10a87f612e299a6eb208210bc0898092a64c48091327cc2aaeee9b764ffa", prefix=sos.revisionFolder(0, 1)) # line 1103 - _.createFile(1) # line 1104 - try: # should exit with error due to collision detection # line 1105 - sos.commit() # should exit with error due to collision detection # line 1105 - _.fail() # should exit with error due to collision detection # line 1105 - except SystemExit as E: # TODO will capture exit(0) which is wrong, change to check code in all places # line 1106 - _.assertEqual(1, E.code) # TODO will capture exit(0) which is wrong, change to check code in all places # line 1106 - - def testFindBase(_): # line 1108 - old = os.getcwd() # line 1109 - try: # line 1110 - os.mkdir("." + os.sep + ".git") # line 1111 - os.makedirs("." + os.sep + "a" + os.sep + sos.metaFolder) # line 1112 - os.makedirs("." + os.sep + "a" + os.sep + "b") # line 1113 - os.chdir("a" + os.sep + "b") # line 1114 - s, vcs, cmd = sos.findSosVcsBase() # line 1115 - _.assertIsNotNone(s) # line 1116 - _.assertIsNotNone(vcs) # line 1117 - _.assertEqual("git", cmd) # line 1118 - finally: # line 1119 - os.chdir(old) # line 1119 + def testAskUpdate(_): # line 1079 + _.createFile(1) # line 1080 + _.createFile(3) # line 1081 + _.createFile(5) # line 1082 + sos.offline() # branch 0: only file1 # line 1083 + sos.branch() # line 1084 + os.unlink("file1") # line 1085 + os.unlink("file3") # line 1086 + os.unlink("file5") # line 1087 + _.createFile(2) # line 1088 + _.createFile(4) # line 1089 + _.createFile(6) # line 1090 + sos.commit() # branch 1: only file2 # line 1091 + sos.switch("0/") # line 1092 + mockInput(["y", "a", "y", "a"], lambda _=None: sos.update("1/", ["--ask"])) # line 1093 + _.assertFalse(_.existsFile(1)) # line 1094 + _.assertFalse(_.existsFile(3)) # line 1095 + _.assertFalse(_.existsFile(5)) # line 1096 + _.assertTrue(_.existsFile(2)) # line 1097 + _.assertTrue(_.existsFile(4)) # line 1098 + _.assertTrue(_.existsFile(6)) # line 1099 + + def testHashCollision(_): # line 1101 + sos.offline() # line 1102 + _.createFile(1) # line 1103 + os.mkdir(sos.revisionFolder(0, 1)) # line 1104 + _.createFile("b9ee10a87f612e299a6eb208210bc0898092a64c48091327cc2aaeee9b764ffa", prefix=sos.revisionFolder(0, 1)) # line 1105 + _.createFile(1) # line 1106 + try: # should exit with error due to collision detection # line 1107 + sos.commit() # should exit with error due to collision detection # line 1107 + _.fail() # should exit with error due to collision detection # line 1107 + except SystemExit as E: # TODO will capture exit(0) which is wrong, change to check code in all places # line 1108 + _.assertEqual(1, E.code) # TODO will capture exit(0) which is wrong, change to check code in all places # line 1108 + + def testFindBase(_): # line 1110 + old = os.getcwd() # line 1111 + try: # line 1112 + os.mkdir("." + os.sep + ".git") # line 1113 + os.makedirs("." + os.sep + "a" + os.sep + sos.metaFolder) # line 1114 + os.makedirs("." + os.sep + "a" + os.sep + "b") # line 1115 + os.chdir("a" + os.sep + "b") # line 1116 + s, vcs, cmd = sos.findSosVcsBase() # line 1117 + _.assertIsNotNone(s) # line 1118 + _.assertIsNotNone(vcs) # line 1119 + _.assertEqual("git", cmd) # line 1120 + finally: # line 1121 + os.chdir(old) # line 1121 # TODO test command line operation --sos vs. --vcs # check exact output instead of only expected exception/fail @@ -1335,6 +1337,6 @@ def testFindBase(_): # line 1108 # TODO tests for loadcommit redirection # TODO test wrong branch/revision after fast branching, would raise exception for -1 otherwise -if __name__ == '__main__': # line 1129 - logging.basicConfig(level=logging.DEBUG, stream=sys.stderr, format="%(asctime)-23s %(levelname)-8s %(name)s:%(lineno)d | %(message)s" if '--log' in sys.argv else "%(message)s") # line 1130 - unittest.main(testRunner=debugTestRunner() if '-v' in sys.argv and not os.getenv("CI", "false").lower() == "true" else None) # warnings = "ignore") # line 1131 +if __name__ == '__main__': # line 1131 + logging.basicConfig(level=logging.DEBUG, stream=sys.stderr, format="%(asctime)-23s %(levelname)-8s %(name)s:%(lineno)d | %(message)s" if '--log' in sys.argv else "%(message)s") # line 1132 + unittest.main(testRunner=debugTestRunner() if '-v' in sys.argv and not os.getenv("CI", "false").lower() == "true" else None) # warnings = "ignore") # line 1133 diff --git a/sos/usage.coco b/sos/usage.coco index f68405b..18dbb62 100644 --- a/sos/usage.coco +++ b/sos/usage.coco @@ -42,7 +42,7 @@ COMMANDS:Dict[str,Command] = { "offline": Command(Category.Repository_handling, [ Argument("[", "Name of the initial branch to use. Default: determined by the type of the underlying VCS"), - Argument("[]]", "Initial commit message. Default: A timestamp") + Argument("[] ]", "Initial commit message. Default: A timestamp") ], "Prepare working offline with SOS, creating an initial branch from the current file tree", """Creates the offline repository metadata in a folder ".sos/" relative to the current working directory. @@ -64,7 +64,9 @@ COMMANDS:Dict[str,Command] = { ), "help": Command(Category.Further_commands, [ - Argument("[ | ]", """Name of command or command category to get help for. Category is one out of "repo", "branches", "files", "patterns", or "other".""") + Argument("[ | ]", """Name of command or command category to get help for. + Command is one out of everything shown per "sos help". + Category is one out of "repo", "branches", "files", "patterns", or "other".""") ], "Display usage and background information", """ The help command provides a compact usage and interface guide to SOS. @@ -91,8 +93,8 @@ COMMANDS:Dict[str,Command] = { ), "branch": Command(Category.Working_with_branches, [ - Argument("[]", "Name of the new branch to create"), - Argument("[]", "Initial commit message for the new branch") + Argument("[", "Name of the new branch to create"), + Argument("[] ]", "Initial commit message for the new branch") ], "Create a new branch", """Create a new branch and switch to work on it. diff --git a/sos/usage.py b/sos/usage.py index 267a1dc..7b40759 100644 --- a/sos/usage.py +++ b/sos/usage.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# __coconut_hash__ = 0xf0610e6 +# __coconut_hash__ = 0x351ecac4 # Compiled with Coconut version 1.3.1-post_dev28 [Dead Parrot] @@ -61,16 +61,18 @@ def __eq__(self, other): # line 38 -COMMANDS = {"offline": Command(Category.Repository_handling, [Argument("[", "Name of the initial branch to use. Default: determined by the type of the underlying VCS"), Argument("[]]", "Initial commit message. Default: A timestamp")], "Prepare working offline with SOS, creating an initial branch from the current file tree", """Creates the offline repository metadata in a folder ".sos/" relative to the current working directory. +COMMANDS = {"offline": Command(Category.Repository_handling, [Argument("[", "Name of the initial branch to use. Default: determined by the type of the underlying VCS"), Argument("[] ]", "Initial commit message. Default: A timestamp")], "Prepare working offline with SOS, creating an initial branch from the current file tree", """Creates the offline repository metadata in a folder ".sos/" relative to the current working directory. The existence of this metadata folder marks the root for the offline SOS repository"""), "online": Command(Category.Repository_handling, [], "Finish working online, removing the SOS repository's metadata folder", """The user is warned if any branches remain that have not been committed/pushed to the underlying VCS. If not, or using the "--force" option, the "./sos" folder is removed entirely. SOS will serve again only as a pass-through command for the original underlying VCS in that folder"""), "dump": Command(Category.Repository_handling, [Argument("[/]", "File name for the exported repository archive dump")], "Perform repository dump into an archive file", """The archive will contain only the metadata folder, not the file tree. - After unzipping the archive, the file tree can be easily restored to the latest revision with "sos switch /" """), "help": Command(Category.Further_commands, [Argument("[ | ]", """Name of command or command category to get help for. Category is one out of "repo", "branches", "files", "patterns", or "other".""")], "Display usage and background information", """ The help command provides a compact usage and interface guide to SOS. + After unzipping the archive, the file tree can be easily restored to the latest revision with "sos switch /" """), "help": Command(Category.Further_commands, [Argument("[ | ]", """Name of command or command category to get help for. + Command is one out of everything shown per "sos help". + Category is one out of "repo", "branches", "files", "patterns", or "other".""")], "Display usage and background information", """ The help command provides a compact usage and interface guide to SOS. For further information, read the online help at https://sos-vcs.net"""), "version": Command(Category.Further_commands, [], "Display SOS version and packing information", """Show information about the SOS source revision, plus the version identifier of the PyPI package and Git commit status"""), "log": Command(Category.Working_with_branches, [], "List revisions of current branch", """List all revisions of currently selected branch in chronological order (latest last). The format for each log entry is as follows: [*] r @ (+/-/~/T) ||"""), "status": Command(Category.Repository_handling, [Argument("[][/]", """Branch and/or revision to show changes of file tree against (if "useChangesCommand" flag is disabled). The argument is ignored (if "useChangesCommand" is enabled)""")], "Display file tree changes or repository stats", """Display changed filepaths vs. last committed or specified revision on current or specified branch (if "useChangesCommand" flag is disabled). - Display repository stats and settings (is "useChangesCommand" flag is enabled)"""), "branch": Command(Category.Working_with_branches, [Argument("[]", "Name of the new branch to create"), Argument("[]", "Initial commit message for the new branch")], "Create a new branch", """Create a new branch and switch to work on it. + Display repository stats and settings (is "useChangesCommand" flag is enabled)"""), "branch": Command(Category.Working_with_branches, [Argument("[", "Name of the new branch to create"), Argument("[] ]", "Initial commit message for the new branch")], "Create a new branch", """Create a new branch and switch to work on it. Default: Use current file tree as basis, using automatically generated branch name and initial commit message."""), "destroy": Command(Category.Working_with_branches, [Argument("[]", "Name or index of the branch")], "Remove branch", """The current or specified branch will be removed entirely from the SOS metadata. There will be a backup, however, that can be restored manually"""), "switch": Command(Category.Working_with_branches, [Argument("[][/]", "Branch and/or revision to switch to")], "Switch to another branch", """Replace file tree contents by specified revision of current or specified other branch. This command will warn the user about changes in the file tree vs. the last committed revision on the current branch. After switching, all changes done to the file tree will be compared to the (new) current branch's latest committed revision"""), "ls": Command(Category.Working_with_files, [Argument("[]", "Path to list")], "List files, display tracking status, and show changes", """Lists file in current or specified folder, showing modification status, and matching tracking patterns (if in track or picky mode)"""), "commit": Command(Category.Working_with_files, [Argument("[]", "Message to store with the revision. Can be used to refer to the revision, and is also shown in the " "log" " command")], "Create a new revision", """Using the current file tree, create and persist a new revision of the current branch to the repository"""), "update": Command(Category.Working_with_files, [Argument("[][/]", "Branch and/or revision")], "Integrate work from another branch into the file tree", """Similarly to switch, this command updates the current file tree to the state of another revision, usually from another branch." @@ -102,77 +104,77 @@ def __eq__(self, other): # line 38 Cannot be changed via user interface after repository creation. Most commands, however, support a "--strict" option nevertheless"""}, "force": {None: """Executes potentially harmful operations, telling SOS that you really intend to perform that command. Most commands: Ignore uncommitted branches, continue to remove SOS repository metadata folders """, "offline": """If already in offline mode, remove offline repository first before creating empty offline repository anew""", "online": """Ignore uncommitted branches, continue to remove SOS repository metadata folder""", "destroy": """Ignore dirty branches (those with changes not committed back to the underlying VCS) and continue with branch destruction""", "switch": """Override safety check to break switching when file tree contains modifications"""}, "full": {"dump": """Force a full repository dump instead of a differential export"""}, "skip-backup": {"dump": "Don't create a backup of a previous dump archive before dumping the repository" ""}, "changes": {"log": "List differential changeset for each revision"}, "diff": {"log": "Display textual diff for each revision"}, "repo": {"status": """List branches and display repository status (regardless of "useChangesCommand" flag)"""}, "stay": {"branch": "Perform branch operation, but don't switch to newly created branch"}, "last": {"branch": "Use last revision instead of current file tree as basis for new branch. Doesn't affect current file tree"}, "fast": {"branch": "Use the experimental fast branch method. Always implies --last"}, "meta": {"switch": "Only switch the branch's file tracking patterns when switching the branch. Won't update any files"}, "progress": {None: """Display file names during file tree traversal, show processing speed, and show compression advantage, if the "compress" flag is enabled"""}, "log": {None: """Configures the Python logging module to include source details like log level, timestamp, module, and line number with the logged messages"""}, "verbose": {None: "Enable more verbose user output"}, "debug": {None: "Enable logging of internal details (intended for developers only)"}, "only ": {None: """Restrict operation to specified already tracked tracking pattern(s). Available for commands "changes", "commit", "diff", "switch", and "update" """}, "except ": {None: """Avoid operation for specified already tracked tracking pattern(s). Available for commands "changes", "commit", "diff", "switch", and "update" """}, "patterns": {"ls": "Only show tracking patterns"}, "tags": {"ls": "List all repository tags (has nothing to do with file or filepattern listing)"}, "recursive": {"ls": "Recursively list also files in sub-folders"}, "r": {"ls": "Recursively list also files in sub-folders"}, "all": {"ls": "Recursively list all files, starting from repository root", "log": """Show all commits since creation of the branch. - Default is only showing the last "logLines" entries"""}, "a": {"ls": "Recursively list all files, starting from repository root"}, "tag": {"commit": "Store the commit message as a tag that can be used instead of numeric revisions"}, "add": {"switch": "Only add new files"}, "add-lines": {"switch": "Only add inserted lines"}, "add-chars": {"switch": "Only add new characters"}, "rm": {"switch": "Only remove vanished files"}, "rm-lines": {"switch": "Only remove deleted lines"}, "rm-chars": {"switch": "Only remove vanished characters"}, "ask": {"switch": "Ask how to proceed with modified files"}, "ask-lines": {"switch": "Ask how to proceed with modified lines"}, "ask-chars": {"switch": "Ask how to proceed with modified characters"}, "eol": {"switch": "Use EOL style from the integrated file instead. Default: EOL style of current file"}, "ignore-whitespace": {"diff": "Ignore white spaces during comparison"}, "wrap": {"diff": "Wrap text around terminal instead of cropping into terminal width"}, "soft": {"mv": "Don't move or rename files, only affect the tracking pattern"}, "local": {"config set": "Persist configuration setting in local repository, not in user-global settings store"}, "local": {"config unset": "Persist configuration setting in local repository, not in user-global settings store"}, "local": {"config add": "Persist configuration setting in local repository, not in user-global settings store"}, "local": {"config rm": "Persist configuration setting in local repository, not in user-global settings store"}, "local": {"config show": "Only show configuration settings persisted in local repository, not from user-global settings store"}, "prune": {"config rm": "Remove a list-type parameter together with the last entry"}, "sos": {None: """Pass command and arguments to SOS, even when not in offline mode, e.g. "sos --sos config set key value" to avoid passing the command to Git or SVN"""}, "n": {"log": """Maximum number of entries to show"""}} # type: Dict[str, Dict[_coconut.typing.Optional[str], str]] # line 260 - - -def getTitleFont(text: 'str', width: 'int') -> 'Tuple[str, str]': # line 430 - ''' Finds best fitting font for termimal window width, falling back to SOS marker if nothing fits current terminal width. Returns (actual text, selected Figlet font). ''' # line 431 - x = sorted((t for t in [(max((len(_) for _ in Figlet(font=f, width=999).renderText(text).split("\n"))), f) for f in ["big", "modular", "bell", "nscript", "pebbles", "puffy", "roman", "rounded", "santaclara", "script", "small", "soft", "standard", "univers", "thin"]] if t[0] <= width)) # type: List[Tuple[int, str]] # line 432 - if len(x) == 0: # replace by shortest text # line 433 - text = MARKER # replace by shortest text # line 433 - return (text, sorted((t for t in [(max((len(_) for _ in Figlet(font=f, width=999).renderText(text).split("\n"))), f) for f in ["big", "modular", "bell", "nscript", "pebbles", "puffy", "roman", "rounded", "santaclara", "script", "small", "soft", "standard", "univers", "thin"]] if t[0] <= width))[-1][1]) # line 434 - -@_coconut_tco # https://github.com/pwaller/pyfiglet/blob/master/doc/figfont.txt # line 436 -def getTitle(large: 'bool'=True) -> '_coconut.typing.Optional[str]': # https://github.com/pwaller/pyfiglet/blob/master/doc/figfont.txt # line 436 - ''' Large: use ascii-art. ''' # line 437 - if not large: # line 438 - return APP # line 438 - if not Figlet: # line 439 - return None # line 439 - text, font = getTitleFont(APP, width=pure.termWidth) # line 440 - return _coconut_tail_call("\n".join, (_ for _ in Figlet(font=font, width=pure.termWidth).renderText(text).split("\n") if _.replace(" ", "") != "")) # line 441 - -def usage(argument: 'str', version: 'bool'=False, verbose: 'bool'=False): # line 443 - if version: # line 444 - title = getTitle() # type: _coconut.typing.Optional[str] # line 445 - if title: # line 446 - print(title + "\n") # line 446 - print("%s%s%s" % (MARKER, APPNAME if version else APP, "" if not version else " (PyPI: %s)" % VERSION)) # line 447 - if version: # line 448 - sys.exit(0) # line 448 - category = CategoryAbbrev.get(argument, None) # type: _coconut.typing.Optional[Category] # convert shorthand for category # line 449 - command = argument if category is None else None # type: _coconut.typing.Optional[str] # line 450 - if command is None: # line 451 - print("\nUsage:\n sos [, []] [, [ 0 else 0 # type: int # argument name length max plus indentation # line 461 - for c in cmd.arguments: # line 462 - print(pure.ljust(" %s " % c.name, maxlen) + ("\n" + pure.ljust(width=maxlen)).join(pure.splitStrip(c.long))) # line 462 - matchingoptions = [(optname, pure.splitStrip(description)) for optname, description in [(optname, dikt[name]) for optname, dikt in OPTIONS.items() if name in dikt]] # type: List[Tuple[str, _coconut.typing.Sequence[str]]] # line 463 - if matchingoptions: # line 464 - print("\n Options:") # line 465 - maxoptlen = max([len(optname) for optname, __ in matchingoptions]) # type: int # line 466 - for optname, descriptions in sorted(matchingoptions): # line 467 - if len(descriptions) == 0: # line 468 - continue # line 468 - print(" %s%s %s%s" % ("--" if len(optname) > 1 else "-", pure.ljust(optname, maxoptlen + (0 if len(optname) > 1 else 1)), descriptions[0], "\n" + pure.ajoin(" " * (6 + maxoptlen + (2 if len(optname) > 1 else 1)), descriptions[1:], nl="\n") if len(descriptions) > 1 else "")) # line 469 - matchingoptions = [] if cmd is None else [(optname, pure.splitStrip(dikt[None]) if None in dikt else []) for optname, dikt in OPTIONS.items()] # add all text for the generic description # line 470 - if matchingoptions: # line 471 - print("\n Common options:") # line 472 - maxoptlen = max([len(optname) for optname, __ in matchingoptions]) # line 473 - for optname, descriptions in sorted(matchingoptions): # line 474 - if len(descriptions) == 0: # line 475 - continue # line 475 - print(" %s%s %s%s" % ("--" if len(optname) > 1 else "-", pure.ljust(optname, maxoptlen + (0 if len(optname) > 1 else 1)), descriptions[0], "\n" + pure.ajoin(" " * (6 + maxoptlen + (2 if len(optname) > 1 else 1)), descriptions[1:], nl="\n") if len(descriptions) > 1 else "")) # line 476 - if command is None: # line 477 - print("\nCommon options:") # line 478 - genericOptions = {k: v[None] for k, v in OPTIONS.items() if None in v} # type: Dict[str, str] # line 479 - maxlen = max((len(_) for _ in genericOptions)) # line 480 - for optname, description in sorted(genericOptions.items()): # line 481 - print(" %s%s %s" % ("--" if len(optname) > 1 else "-", pure.ljust(optname, maxlen), pure.ajoin(" " * (2 + 2 + maxlen + 2), pure.splitStrip(description), nl="\n", first=False))) # line 482 + Default is only showing the last "logLines" entries"""}, "a": {"ls": "Recursively list all files, starting from repository root"}, "tag": {"commit": "Store the commit message as a tag that can be used instead of numeric revisions"}, "add": {"switch": "Only add new files"}, "add-lines": {"switch": "Only add inserted lines"}, "add-chars": {"switch": "Only add new characters"}, "rm": {"switch": "Only remove vanished files"}, "rm-lines": {"switch": "Only remove deleted lines"}, "rm-chars": {"switch": "Only remove vanished characters"}, "ask": {"switch": "Ask how to proceed with modified files"}, "ask-lines": {"switch": "Ask how to proceed with modified lines"}, "ask-chars": {"switch": "Ask how to proceed with modified characters"}, "eol": {"switch": "Use EOL style from the integrated file instead. Default: EOL style of current file"}, "ignore-whitespace": {"diff": "Ignore white spaces during comparison"}, "wrap": {"diff": "Wrap text around terminal instead of cropping into terminal width"}, "soft": {"mv": "Don't move or rename files, only affect the tracking pattern"}, "local": {"config set": "Persist configuration setting in local repository, not in user-global settings store"}, "local": {"config unset": "Persist configuration setting in local repository, not in user-global settings store"}, "local": {"config add": "Persist configuration setting in local repository, not in user-global settings store"}, "local": {"config rm": "Persist configuration setting in local repository, not in user-global settings store"}, "local": {"config show": "Only show configuration settings persisted in local repository, not from user-global settings store"}, "prune": {"config rm": "Remove a list-type parameter together with the last entry"}, "sos": {None: """Pass command and arguments to SOS, even when not in offline mode, e.g. "sos --sos config set key value" to avoid passing the command to Git or SVN"""}, "n": {"log": """Maximum number of entries to show"""}} # type: Dict[str, Dict[_coconut.typing.Optional[str], str]] # line 262 + + +def getTitleFont(text: 'str', width: 'int') -> 'Tuple[str, str]': # line 432 + ''' Finds best fitting font for termimal window width, falling back to SOS marker if nothing fits current terminal width. Returns (actual text, selected Figlet font). ''' # line 433 + x = sorted((t for t in [(max((len(_) for _ in Figlet(font=f, width=999).renderText(text).split("\n"))), f) for f in ["big", "modular", "bell", "nscript", "pebbles", "puffy", "roman", "rounded", "santaclara", "script", "small", "soft", "standard", "univers", "thin"]] if t[0] <= width)) # type: List[Tuple[int, str]] # line 434 + if len(x) == 0: # replace by shortest text # line 435 + text = MARKER # replace by shortest text # line 435 + return (text, sorted((t for t in [(max((len(_) for _ in Figlet(font=f, width=999).renderText(text).split("\n"))), f) for f in ["big", "modular", "bell", "nscript", "pebbles", "puffy", "roman", "rounded", "santaclara", "script", "small", "soft", "standard", "univers", "thin"]] if t[0] <= width))[-1][1]) # line 436 + +@_coconut_tco # https://github.com/pwaller/pyfiglet/blob/master/doc/figfont.txt # line 438 +def getTitle(large: 'bool'=True) -> '_coconut.typing.Optional[str]': # https://github.com/pwaller/pyfiglet/blob/master/doc/figfont.txt # line 438 + ''' Large: use ascii-art. ''' # line 439 + if not large: # line 440 + return APP # line 440 + if not Figlet: # line 441 + return None # line 441 + text, font = getTitleFont(APP, width=pure.termWidth) # line 442 + return _coconut_tail_call("\n".join, (_ for _ in Figlet(font=font, width=pure.termWidth).renderText(text).split("\n") if _.replace(" ", "") != "")) # line 443 + +def usage(argument: 'str', version: 'bool'=False, verbose: 'bool'=False): # line 445 + if version: # line 446 + title = getTitle() # type: _coconut.typing.Optional[str] # line 447 + if title: # line 448 + print(title + "\n") # line 448 + print("%s%s%s" % (MARKER, APPNAME if version else APP, "" if not version else " (PyPI: %s)" % VERSION)) # line 449 + if version: # line 450 + sys.exit(0) # line 450 + category = CategoryAbbrev.get(argument, None) # type: _coconut.typing.Optional[Category] # convert shorthand for category # line 451 + command = argument if category is None else None # type: _coconut.typing.Optional[str] # line 452 + if command is None: # line 453 + print("\nUsage:\n sos [, []] [, [ 0 else 0 # type: int # argument name length max plus indentation # line 463 + for c in cmd.arguments: # line 464 + print(pure.ljust(" %s " % c.name, maxlen) + ("\n" + pure.ljust(width=maxlen)).join(pure.splitStrip(c.long))) # line 464 + matchingoptions = [(optname, pure.splitStrip(description)) for optname, description in [(optname, dikt[name]) for optname, dikt in OPTIONS.items() if name in dikt]] # type: List[Tuple[str, _coconut.typing.Sequence[str]]] # line 465 + if matchingoptions: # line 466 + print("\n Options:") # line 467 + maxoptlen = max([len(optname) for optname, __ in matchingoptions]) # type: int # line 468 + for optname, descriptions in sorted(matchingoptions): # line 469 + if len(descriptions) == 0: # line 470 + continue # line 470 + print(" %s%s %s%s" % ("--" if len(optname) > 1 else "-", pure.ljust(optname, maxoptlen + (0 if len(optname) > 1 else 1)), descriptions[0], "\n" + pure.ajoin(" " * (6 + maxoptlen + (2 if len(optname) > 1 else 1)), descriptions[1:], nl="\n") if len(descriptions) > 1 else "")) # line 471 + matchingoptions = [] if cmd is None else [(optname, pure.splitStrip(dikt[None]) if None in dikt else []) for optname, dikt in OPTIONS.items()] # add all text for the generic description # line 472 + if matchingoptions: # line 473 + print("\n Common options:") # line 474 + maxoptlen = max([len(optname) for optname, __ in matchingoptions]) # line 475 + for optname, descriptions in sorted(matchingoptions): # line 476 + if len(descriptions) == 0: # line 477 + continue # line 477 + print(" %s%s %s%s" % ("--" if len(optname) > 1 else "-", pure.ljust(optname, maxoptlen + (0 if len(optname) > 1 else 1)), descriptions[0], "\n" + pure.ajoin(" " * (6 + maxoptlen + (2 if len(optname) > 1 else 1)), descriptions[1:], nl="\n") if len(descriptions) > 1 else "")) # line 478 + if command is None: # line 479 + print("\nCommon options:") # line 480 + genericOptions = {k: v[None] for k, v in OPTIONS.items() if None in v} # type: Dict[str, str] # line 481 + maxlen = max((len(_) for _ in genericOptions)) # line 482 + for optname, description in sorted(genericOptions.items()): # line 483 + print(" %s%s %s" % ("--" if len(optname) > 1 else "-", pure.ljust(optname, maxlen), pure.ajoin(" " * (2 + 2 + maxlen + 2), pure.splitStrip(description), nl="\n", first=False))) # line 484 # TODO wrap text at terminal boundaries automatically, if space suffices # [][/] Revision string. Branch is optional (defaulting to current branch) and may be a label or number >= 0 # Revision is an optional integer and may be negative to reference from the latest commits (-1 is most recent revision), or a tag name""" - sys.exit(0) # line 487 + sys.exit(0) # line 489 diff --git a/sos/utility.coco b/sos/utility.coco index 1524dd0..35e71b6 100644 --- a/sos/utility.coco +++ b/sos/utility.coco @@ -99,8 +99,8 @@ DOT_SYMBOL:str = "\u00b7" MULT_SYMBOL:str = "\u00d7" CROSS_SYMBOL:str = "\u2716" CHECKMARK_SYMBOL:str = "\u2714" -PLUSMINUS_SYMBOL:str = "\u00b1" -MOVE_SYMBOL:str = "\u21cc" # \U0001F5C0" # HINT second one is very unlikely to be in any console font +PLUSMINUS_SYMBOL:str = "\u00b1" # alternative for "~" +MOVE_SYMBOL:str = "\u21cc" # alternative for "#". or use \U0001F5C0", which is very unlikely to be in any console font METADATA_FORMAT:int = 1 # counter for incompatible consecutive formats (was undefined, "1" is the first versioned version after that) vcsFolders:Dict[str,str] = {".svn": SVN, ".git": "git", ".bzr": "bzr", ".hg": "hg", ".fslckout": "fossil", "_FOSSIL_": "fossil", ".CVS": "cvs", "_darcs": "darcs", "_MTN": "monotone", ".git/GL_COMMIT_EDIT_MSG": "gl"} vcsBranches:Dict[str,str?] = {SVN: "trunk", "git": "master", "bzr": "trunk", "hg": "default", "fossil": None, "cvs": None, "darcs": None, "monotone": None} @@ -185,8 +185,8 @@ def revisionFolder(branch:int, revision:int, base:str? = None, file:str? = None) def Exit(message:str = "", code = 1): printe("[EXIT%s]" % (" %.1fs" % (time.time() - START_TIME) if verbose else "") + (" " + message + "." if message != "" else "")); sys.exit(code) -def fitStrings(strings:str[], prefix:str, length:int = MAX_COMMAND_LINE.get(sys.platform, MAX_COMMAND_LINE[None]), separator:str = " ", process:-> str = -> '"%s"' % _) -> str = - ''' Returns a packed string, destructively consuming entries from the provided list. Does similar to xargs. getconf ARG_MAX or xargs --show-limits. ''' +def fitStrings(strings:str[], prefix:str, length:int = MAX_COMMAND_LINE.get(sys.platform, MAX_COMMAND_LINE[None]), separator:str = " ", process: -> str = -> '"%s"' % _) -> str = + ''' Returns a packed string, destructively consuming entries from the provided list. Does similar as xargs. getconf ARG_MAX or xargs --show-limits. ''' if len(prefix + separator + (strings[0] |> process)) > length: raise Exception("Cannot possibly strings pack into specified length") while len(strings) > 0 and len(prefix + separator + (strings[0] |> process)) <= length: prefix += separator + (strings.pop(0) |> process) prefix @@ -440,9 +440,6 @@ def findSosVcsBase() -> Tuple[str?,str?,str?] = if vcs[0]: return (path, vcs[0], vcs[1]) # already detected vcs base and command sos = path while True: # continue search for VCS base - new = os.path.dirname(path) # get parent path - if new == path: return (sos, None, None) # no VCS folder found - path = new contents = set(os.listdir(path)) vcss = [executable for folder, executable in vcsFolders.items() if folder in contents] # determine VCS type choice = None @@ -451,6 +448,9 @@ def findSosVcsBase() -> Tuple[str?,str?,str?] = warn("Detected more than one parallel VCS checkouts %r. Falling back to '%s'" % (vcss, choice)) elif len(vcss) > 0: choice = vcss[0] if choice: return (sos, path, choice) + new = os.path.dirname(path) # get parent path + if new == path: return (sos, None, None) # no VCS folder found + path = new (None, vcs[0], vcs[1]) def tokenizeGlobPattern(pattern:str) -> List[GlobBlock] = diff --git a/sos/utility.py b/sos/utility.py index fdb6c46..0d1f516 100644 --- a/sos/utility.py +++ b/sos/utility.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# __coconut_hash__ = 0xbe778104 +# __coconut_hash__ = 0x1834478 # Compiled with Coconut version 1.3.1-post_dev28 [Dead Parrot] @@ -177,8 +177,8 @@ def error(_, *s): # line 76 MULT_SYMBOL = "\u00d7" # type: str # line 99 CROSS_SYMBOL = "\u2716" # type: str # line 100 CHECKMARK_SYMBOL = "\u2714" # type: str # line 101 -PLUSMINUS_SYMBOL = "\u00b1" # type: str # line 102 -MOVE_SYMBOL = "\u21cc" # type: str # \U0001F5C0" # HINT second one is very unlikely to be in any console font # line 103 +PLUSMINUS_SYMBOL = "\u00b1" # type: str # alternative for "~" # line 102 +MOVE_SYMBOL = "\u21cc" # type: str # alternative for "#". or use \U0001F5C0", which is very unlikely to be in any console font # line 103 METADATA_FORMAT = 1 # type: int # counter for incompatible consecutive formats (was undefined, "1" is the first versioned version after that) # line 104 vcsFolders = {".svn": SVN, ".git": "git", ".bzr": "bzr", ".hg": "hg", ".fslckout": "fossil", "_FOSSIL_": "fossil", ".CVS": "cvs", "_darcs": "darcs", "_MTN": "monotone", ".git/GL_COMMIT_EDIT_MSG": "gl"} # type: Dict[str, str] # line 105 vcsBranches = {SVN: "trunk", "git": "master", "bzr": "trunk", "hg": "default", "fossil": None, "cvs": None, "darcs": None, "monotone": None} # type: Dict[str, _coconut.typing.Optional[str]] # line 106 @@ -615,20 +615,20 @@ def findSosVcsBase() -> 'Tuple[_coconut.typing.Optional[str], _coconut.typing.Op return (path, vcs[0], vcs[1]) # already detected vcs base and command # line 440 sos = path # line 441 while True: # continue search for VCS base # line 442 - new = os.path.dirname(path) # get parent path # line 443 - if new == path: # no VCS folder found # line 444 - return (sos, None, None) # no VCS folder found # line 444 - path = new # line 445 - contents = set(os.listdir(path)) # line 446 - vcss = [executable for folder, executable in vcsFolders.items() if folder in contents] # determine VCS type # line 447 - choice = None # line 448 - if len(vcss) > 1: # line 449 - choice = SVN if SVN in vcss else vcss[0] # line 450 - warn("Detected more than one parallel VCS checkouts %r. Falling back to '%s'" % (vcss, choice)) # line 451 - elif len(vcss) > 0: # line 452 - choice = vcss[0] # line 452 - if choice: # line 453 - return (sos, path, choice) # line 453 + contents = set(os.listdir(path)) # line 443 + vcss = [executable for folder, executable in vcsFolders.items() if folder in contents] # determine VCS type # line 444 + choice = None # line 445 + if len(vcss) > 1: # line 446 + choice = SVN if SVN in vcss else vcss[0] # line 447 + warn("Detected more than one parallel VCS checkouts %r. Falling back to '%s'" % (vcss, choice)) # line 448 + elif len(vcss) > 0: # line 449 + choice = vcss[0] # line 449 + if choice: # line 450 + return (sos, path, choice) # line 450 + new = os.path.dirname(path) # get parent path # line 451 + if new == path: # no VCS folder found # line 452 + return (sos, None, None) # no VCS folder found # line 452 + path = new # line 453 return (None, vcs[0], vcs[1]) # line 454 def tokenizeGlobPattern(pattern: 'str') -> 'List[GlobBlock]': # line 456 diff --git a/sos/version.py b/sos/version.py index bdd4f30..79f82ac 100755 --- a/sos/version.py +++ b/sos/version.py @@ -1,3 +1,3 @@ -__version_info__ = (2018, 1419, 3131) -__version__ = r'2018.1419.3131-v1.5.0-46-gce6dcdb' +__version_info__ = (2018, 1425, 3206) +__version__ = r'2018.1425.3206-v1.5.0-47-gfc41f16' __release_version__ = '1.5.3' \ No newline at end of file