diff --git a/README.md b/README.md index 210e9d4..5f0ba51 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Subversion Offline Solution (SOS 1.1.2) # +# Subversion Offline Solution (SOS 1.1.3) # [![Travis badge](https://travis-ci.org/ArneBachmann/sos.svg?branch=master)](https://travis-ci.org/ArneBachmann/sos) [![Build status](https://ci.appveyor.com/api/projects/status/fe915rtx02buqe4r?svg=true)](https://ci.appveyor.com/project/ArneBachmann/sos) @@ -6,7 +6,7 @@ [![PyPI badge](https://img.shields.io/pypi/v/sos-vcs.svg)](https://badge.fury.io/py/sos-vcs) - License: [MPL-2.0](https://www.mozilla.org/en-US/MPL/2.0/) -- [Documentation](http://sos-vcs.net), [Code Repository](https://github.com/ArneBachmann/sos) +- [Documentation](http://sos-vcs.net) (official website), [Code Repository](https://github.com/ArneBachmann/sos) (at Github) - [Buy a coffee](http://PayPal.Me/ArneBachmann/) for the developer to show your appreciation! ### List of Abbreviations and Definitions ### @@ -139,14 +139,8 @@ By means of the `sos config set ` command, you can set these flags - `sos update` will **not warn** if local changes are present! This is a noteworthy exception to the failsafe approach taken for most other commands -## FAQ ## -> Q: I don't want to risk data loss in case SOS has some undiscovered bugs. What can I do? -> -> A: Configure SOS to store all versioned files as plain file copies instead of compressed artifacts: `sos offline --plain` for one repository only, or `sos config set compress off` to define a user-preset before going offline. Plain repositories simply copy files when branching and/or versioning; note, however, that filenames will be hashed and stored in the metadata file instead (which is human-readable, thankfully). - - ## Hints and Tipps ## -- Too speed up going offline, use the `sos offline --plain` option: It may reduce the time for going offline by a larger factor (in tests on a small laptop with BTRFS it was 30 times faster due to avoiding Python having to compress all versioned file contents) +- To save space when going offline, use the option `sos offline --compress`: It may increase the time for going offline by a larger factor (e.g. 10x), but will also reduce the amount of storage needed to version files. To enable this option for all offline repositories, use `sos config set compress on` - When specifying file patterns including glob markers on the command line, make sure you quote them correctly. On linux (bash, sh, zsh), put your patterns into quote (`"`), otherwise the shell will replace file patterns by any matching filenames instead of forwarding the pattern literally to SOS - Many commands can be shortened to three, two or even one initial letters - It might in some cases be a good idea to go offline one folder higher up in the file tree than your base working folder to care for potential deletions or renames diff --git a/setup.py b/setup.py index 7942c7c..adc15d8 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import os, shutil, subprocess, sys, time, unittest from setuptools import setup, find_packages -RELEASE = "1.1.2" +RELEASE = "1.1.3" print("sys.argv is %r" % sys.argv) readmeFile = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'README.md') @@ -42,7 +42,7 @@ import sos.sos as sos -if 'test' in sys.argv : print("Warning: Won't build distribution after running unit tests") +if 'test' in sys.argv : print("Warning: Won't create distribution archive after running unit tests") if 'sdist' in sys.argv: print("Cleaning up old archives for twine upload") diff --git a/sos/sos.coco b/sos/sos.coco index 6f18e39..612e196 100644 --- a/sos/sos.coco +++ b/sos/sos.coco @@ -3,7 +3,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. # Standard modules -import codecs, collections, fnmatch, json, logging, mimetypes, os, shutil, sys, time +import time; START_TIME = time.time() +import codecs, collections, fnmatch, json, logging, mimetypes, os, shutil, sys try: from typing import Any, Dict, FrozenSet, IO, Iterator, List, Set, Tuple, Type, Union # only required for mypy except: pass # typing not available (e.g. Python 2) @@ -23,7 +24,7 @@ except: configr = None # declare as undefined # Constants APPNAME:str = "Subversion Offline Solution V%s (C) Arne Bachmann" % version.__release_version__ -defaults = Accessor({"strict": False, "track": False, "picky": False, "compress": True, "tags": [], "texttype": [], "bintype": [], "ignoreDirs": [".*", "__pycache__"], "ignoreDirsWhitelist": [], "ignores": ["__coconut__.py", "*.bak", "*.py[cdo]", "*.class", ".fslckout"], "ignoresWhitelist": []}) +defaults = Accessor({"strict": False, "track": False, "picky": False, "compress": False, "tags": [], "texttype": [], "bintype": [], "ignoreDirs": [".*", "__pycache__"], "ignoreDirsWhitelist": [], "ignores": ["__coconut__.py", "*.bak", "*.py[cdo]", "*.class", ".fslckout"], "ignoresWhitelist": []}) termWidth = getTermWidth() - 1 # uses curses or returns conservative default of 80 @@ -50,7 +51,7 @@ Usage: {cmd} [] [, ...] When operating in of Available commands: offline [] Start working offline, creating a branch (named ), default name depending on VCS - --plain Don't compress versioned files (same as `sos config set compress off`) + --compress Compress versioned files (same as `sos config set compress on && sos offline`) --track Setup SVN-style mode: users add/remove tracking patterns per branch --picky Setup Git-style mode: users pick files for each operation online Finish working offline @@ -103,6 +104,7 @@ Usage: {cmd} [] [, ...] When operating in of --{cmd} When executing {CMD} not being offline, pass arguments to {CMD} instead (e.g. {cmd} --{cmd} config set key value.) --log Enable logging details --verbose Enable verbose output""".format(appname = APPNAME, cmd = "sos", CMD = "SOS")) + Exit(code = 0) # Main data class #@runtime_validation @@ -429,10 +431,10 @@ def offline(argument:str? = None, options:str[] = []) -> None: else: os.unlink(resource) except: Exit("Cannot reliably remove previous repository contents. Please remove .sos folder manually prior to going offline") m:Metadata = Metadata(os.getcwd()) - if '--plain' in options or not m.c.compress: m.compress = False # plain file copies instead of compressed ones - if '--picky' in options or m.c.picky: m.picky = True # Git-like - elif '--track' in options or m.c.track: m.track = True # Svn-like - if '--strict' in options or m.c.strict: m.strict = True # always hash contents + if '--compress' in options or m.c.compress: m.compress = True # plain file copies instead of compressed ones + if '--picky' in options or m.c.picky: m.picky = True # Git-like + elif '--track' in options or m.c.track: m.track = True # Svn-like + if '--strict' in options or m.c.strict: m.strict = True # always hash contents debug("Preparing offline repository...") m.createBranch(0, argument ?? str(defaults["defaultbranch"]), initialMessage = "Offline repository created on %s" % strftime()) # main branch's name may be None (e.g. for fossil) m.branch = 0 @@ -873,6 +875,7 @@ def parse(root:str, cwd:str): elif command[:1] == "u": update(argument, options, onlys, excps) elif command[:1] == "v": usage(short = True) else: Exit("Unknown command '%s'" % command) + Exit(code = 0) except Exception, RuntimeError as E: print(str(E)) import traceback @@ -880,15 +883,14 @@ def parse(root:str, cwd:str): traceback.print_stack() try: traceback.print_last() except: pass - print("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.") - sys.exit(0) + 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.") def main() -> None: global debug, info, warn, error 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")) _log = Logger(logging.getLogger(__name__)); debug, info, warn, error = _log.debug, _log.info, _log.warn, _log.error for option in (o for o in ['--log', '--verbose', '-v', '--sos'] if o in sys.argv): sys.argv.remove(option) # clean up program arguments - if '--help' in sys.argv or len(sys.argv) < 2: usage(); Exit() + if '--help' in sys.argv or len(sys.argv) < 2: usage() command:str? = sys.argv[1] if len(sys.argv) > 1 else None 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 debug("Found root folders for SOS|VCS: %s|%s" % (root ?? "", vcs ?? "")) @@ -916,7 +918,9 @@ def main() -> None: # Main part -level = logging.DEBUG if os.environ.get("DEBUG", "False").lower() == "true" or '--verbose' in sys.argv or '-v' in sys.argv else logging.INFO +verbose = lateBinding["verbose"] = os.environ.get("DEBUG", "False").lower() == "true" or '--verbose' in sys.argv or '-v' in sys.argv # imported from utility, and only modified here +lateBinding["start"] = START_TIME +level = logging.DEBUG if verbose else logging.INFO force_sos:bool = '--sos' in sys.argv force_vcs:bool = '--vcs' in sys.argv _log:logging.Logger = Logger(logging.getLogger(__name__)); debug, info, warn, error = _log.debug, _log.info, _log.warn, _log.error diff --git a/sos/tests.coco b/sos/tests.coco index 49ff3c6..3b5b9e4 100644 --- a/sos/tests.coco +++ b/sos/tests.coco @@ -220,7 +220,7 @@ class Tests(unittest.TestCase): sos.tokenizeGlobPatterns("*xb?c", "*x?bc") # succeeds, because glob patterns match (differ only in position) try: sos.tokenizeGlobPatterns("a???b*", "ab???*"); _.fail() # succeeds, because glob patterns match (differ only in position) except: pass - + def testConvertGlobFiles(_): _.assertEqual(["xxayb", "aacb"], [r[1] for r in sos.convertGlobFiles(["axxby", "aabc"], *sos.tokenizeGlobPatterns("a*b?", "*a?b"))]) _.assertEqual(["1qq2ww3", "1abcbx2xbabc3"], [r[1] for r in sos.convertGlobFiles(["qqxbww", "abcbxxbxbabc"], *sos.tokenizeGlobPatterns("*xb*", "1*2*3"))]) @@ -625,7 +625,7 @@ class Tests(unittest.TestCase): def testCompression(_): _.createFile(1) - sos.offline("master", options = ["--plain", "--force"]) + sos.offline("master", options = ["--force"]) _.assertTrue(_.existsFile(branchFolder(0, 0) + os.sep + "b9ee10a87f612e299a6eb208210bc0898092a64c48091327cc2aaeee9b764ffa", b"x" * 10)) setRepoFlag("compress", True) # was plain = uncompressed before _.createFile(2) @@ -745,8 +745,10 @@ class Tests(unittest.TestCase): sos.delete("added", "--force") # should succeed def testUsage(_): - sos.usage() - sos.usage(short = True) + try: sos.usage(); _.fail() # TODO expect sys.exit(0) + except: pass + try: sos.usage(short = True); _.fail() + except: pass def testOnly(_): _.assertEqual((f{"./A", "x/B"}, f{"./C"}), sos.parseOnlyOptions(".", ["abc", "def", "--only", "A", "--x", "--only", "x/B", "--except", "C", "--only"])) diff --git a/sos/utility.coco b/sos/utility.coco index 15276bc..fd43566 100644 --- a/sos/utility.coco +++ b/sos/utility.coco @@ -50,6 +50,7 @@ SVN = "svn" SLASH = "/" vcsFolders:Dict[str,str] = {".svn": SVN, ".git": "git", ".bzr": "bzr", ".hg": "hg", ".fslckout": "fossil", "_FOSSIL_": "fossil", ".CVS": "cvs"} vcsBranches:Dict[str,str?] = {SVN: "trunk", "git": "master", "bzr": "trunk", "hg": "default", "fossil": None, "cvs": None} +lateBinding:Accessor = Accessor({"verbose": False, "start": 0}) # Value types @@ -109,7 +110,7 @@ def eoldet(file:bytes) -> bytes? = if cr > lf: return b"\r" # older 8-bit machines None # no new line contained, cannot determine -def Exit(message:str = "") -> None: print("[EXIT]" + (" " + message + "." if message != "" else ""), file = sys.stderr); sys.exit(1) +def Exit(message:str = "", code = 1) -> None: print("[EXIT%s]" % (" %.1fs" % (time.time() - lateBinding.start) if lateBinding.verbose else "") + (" " + message + "." if message != "" else ""), file = sys.stderr); sys.exit(1) def user_input(msg:str) -> str = input(msg) # referenceso __builtin__.raw_input on Python 2