Skip to content

Commit

Permalink
Fixes #8
Browse files Browse the repository at this point in the history
  • Loading branch information
ArneBachmann committed Dec 25, 2017
1 parent 1450881 commit 576c32c
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 17 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,35 @@ SOS supports three different file handling models that you may use to your likin
- **Tracking mode**: Only files that match certain file name tracking patterns are respected during `commit`, `update` and `branch` (just like in SVN, gitless, and Fossil), requiring users to specifically add or remove files per branch. Drawback: Need to declare files to track for every offline repository
- **Picky mode**: Each operation needs the explicit declaration of file name patterns for versioning (like Git does). Drawback: Need to stage files for every single commit

### Unique features of SOS ###
- Initializes repositories by default with the *simple mode*, which makes effortless versioning a piece of cake
- In the optional tracking mode, files are tracked based on glob patterns instead of pure filenames or paths (in a manner comparable to how SVN ignores files)
- In-place command line replacement for traditional VCS that transparently pipes commands to them
- Straightforward and simplified semantics for common VCS operations (`branch`, `commit`, integrate changes)

### Limitations ###
- Designed for use by single user, network synchronization is a non-goal. Don't attempt to use SOS in a shared location, concurrent access to the repository may corrupt your data, as there is currently no locking in place (could be augmented, but it's a non-goal too)
- 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 distribution, except PyPy (TODO). Support for Python 2 is only partial, as the test suite doesn't run through entirely yet, although SOS's programming language Coconut is generally able to transpile to valid Python 2 source code
- SOS is compatible with above mentioned traditional VCSs: SVN, Git, gitless, Bazaar, Mercurial and Fossil
- File name 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


## Latest changes ##
- Version 1.1 published on 2017-12-26:
- [Bug 90](https://github.com/ArneBachmann/sos/issues/90) Removed directories weren't picked up
- [Bug 93](https://github.com/ArneBachmann/sos/issues/93) Picky mode lists any file as added
- [Enhancement 63](https://github.com/ArneBachmann/sos/issues/63) Show more change details in `log` and `status`
- [Enhancement 86](https://github.com/ArneBachmann/sos/issues/86) Renamed command for branch removal to `destroy`
- [Feature 61](https://github.com/ArneBachmann/sos/issues/61) Added option to only consider or exclude certain file patterns for relevant operations using `--only` and `--except`
- [Feature 8](https://github.com/ArneBachmann/sos/issues/8) Added functionality to rename tracking patterns and move files accordingly
- [Feature 80](https://github.com/ArneBachmann/sos/issues/80) Added functionality to use tags
- [QA 79](https://github.com/ArneBachmann/sos/issues/79) Added AppVeyor automated testing
- [QA 94](https://github.com/ArneBachmann/sos/issues/94) More test coverage


## Comparison with Traditional VCS ##
When completing SOS 1.0 I incidentally discovered an interesting article by ... that discusses central weaknesses in the design of VCSs, with a focus on Git. Many of these arguments I have intuitively felt to be true as well and were the reason for the development of SOS: mainly the reduction of barriers between the developer's typical workflow and the VCS, which is most often used as a structured tool for "type and save in increments", while advanced features of Git are just very difficult to remember and get done right.

Expand All @@ -54,6 +76,7 @@ Here is a comparison between SOS and VCS's commands:
- The first revision (created during execution of `sos offline` or `sos branch`) always has the number `0`
- Each `sos commit` increments the revision number by one; revisions are referenced by this numeric index only
- `delete` destroys and removes a branch. It's a command, not an option flag as in `git branch -d <name>`
- `move` renames a file tracking pattern and all matching files accordingly; only useful in tracking or picky mode. It supports reordering of literal substrings, but no reordering of glob markers, and no adjacent glob markers. Use `--soft` to avoid files actually being renamed in the file tree
- `switch` works like `checkout` in Git for a revision of another branch (or of the current), or `update` to latest or a specific revision in SVN. Please note that switching to a different revision will in no way fix or remember that revision. The file tree will always be compared to the branch's latest commit for change detection
- `update` works a bit like `pull` in Git or `update` in SVN and replays the given branch's and/or revision's changes into the file tree. There are plenty of options to configure what changes are actually integrated. This command will not change the current branch like `switch` does

Expand Down
6 changes: 2 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@

readmeFile = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'README.md')
if 'build' in sys.argv:
print("Transpiling Coconut for packaging...")
print("Transpiling Coconut files to Python...")
cmd = "-develop" if 0 == subprocess.Popen("coconut-develop --help", shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE, bufsize = 10000000).wait() and os.getenv("NODEV", "false").strip().lower() != "true" else ""

assert 0 == os.system("coconut%s -p -l -t 3 sos%sutility.coco" % (cmd, os.sep))
assert 0 == os.system("coconut%s -p -l -t 3 sos%ssos.coco" % (cmd, os.sep)) # TODO remove target once Python 2 problems have been fixed
assert 0 == os.system("coconut%s -p -l -t 3 sos%stests.coco" % (cmd, os.sep))
assert 0 == os.system("coconut%s -p -l -t 3 sos" % cmd) # TODO remove target once Python 2 problems have been fixed

if os.path.exists(".git"):
print("Preparing documentation for PyPI by converting from Markdown to reStructuredText via pandoc")
Expand Down
42 changes: 38 additions & 4 deletions sos/sos.coco
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ Usage: {cmd} <command> [<argument>] [<option1>, ...] When operating in of
changes [<branch>][/<revision>] List changed paths vs. last or specified revision
diff [<branch>][/<revision>] List changes vs. last or specified revision
add [<filename or glob pattern>] Add a tracking pattern to current branch (path/filename or glob pattern)
mv [<oldpattern>] [<newPattern>] Rename, move or move and rename tracked files according to glob patterns
mv [<oldpattern>] [<newPattern>] Rename, move, or move and rename tracked files according to glob patterns
--soft Don't move or rename files, only the tracking pattern
rm [<filename or glob pattern>] Remove a tracking pattern. Only useful after "offline --track" or "offline --picky"

ls List file tree and mark changes and tracking status
Expand Down Expand Up @@ -91,8 +92,8 @@ Usage: {cmd} <command> [<argument>] [<option1>, ...] When operating in of
--only <filename or glob> Restrict operation to specified pattern(s). Available for "changes", "commit", "diff", "switch", and "update"
--except <filename or glob> Avoid operation for specified pattern(s). Available for "changes", "commit", "diff", "switch", and "update"
--{cmd} When executing {CMD} not being offline, pass arguments to {CMD} instead (e.g. {cmd} --{cmd} config set key value.)
--log Enable internals logger
--verbose Enable debugging output""".format(appname = APPNAME, cmd = "sos", CMD = "SOS"))
--log Enable logging details
--verbose Enable verbose output""".format(appname = APPNAME, cmd = "sos", CMD = "SOS"))

# Main data class
#@runtime_validation
Expand Down Expand Up @@ -780,7 +781,40 @@ def config(argument:str, options:List[str] = []) -> None:

def move(relPath:str, pattern:str, newRelPath:str, newPattern:str, options:List[str] = []) -> None:
''' Path differs: Move files, create folder if not existing. Pattern differs: Attempt to rename file, unless exists in target or not unique. '''
pass
force:bool = '--force' in options
soft:bool = '--soft' in options
if not os.path.exists(relPath): Exit("Source folder doesn't exist")
matching:str[] = fnmatch.filter(os.listdir(relPath.replace(SLASH, os.sep)), os.path.basename(pattern)) # find matching files in source
if not matching and not force: Exit("No files match the specified file or glob pattern. Use --force to proceed anyway")
m:Metadata = Metadata(os.getcwd())
m.loadBranches() # knows current branch
if not m.track and not m.picky: Exit("Repository is in simple mode. Simply use basic file operations to modify files, then execute 'sos commit' to version the changes")
if pattern not in m.branches[m.branch].tracked:
for tracked in (t for t in m.branches[m.branch].tracked if os.path.dirname(t) == relPath): # for all patterns of the same source folder
alternative:str[] = fnmatch.filter(matching, os.path.basename(tracked)) # find if it matches any of the files in the source folder, too
if alternative: info(" '%s' matches %d files" % (tracked, len(alternative)))
Exit("File or glob pattern '%s' is not tracked on current branch. 'sos move' only works on tracked patterns" % pattern)
basePattern:str = os.path.basename(pattern) # pure glob without folder
newBasePattern:str = os.path.basename(newPattern)
if basePattern.count("*") != newBasePattern.count("*")\
or (basePattern.count("?") - basePattern.count("[?]")) != (newBasePattern.count("?") - newBasePattern.count("[?]"))\
or (basePattern.count("[") - basePattern.count("\\[")) != (newBasePattern.count("[") - newBasePattern.count("\\["))\
or (basePattern.count("]") - basePattern.count("\\]")) != (newBasePattern.count("]") - newBasePattern.count("\\]")):
Exit("Glob markers don't match, cannot move/rename tracked matching files")
oldTokens:GlobBlock[]; newToken:GlobBlock[]
oldTokens, newTokens = tokenizeGlobPatterns(os.path.basename(pattern), os.path.basename(newPattern))
matches:Tuple[str,str][] = convertGlobFiles(matching, oldTokens, newTokens) # computes list of source - target filename pairs
matches = reorderRenameActions(matches, exitOnConflict = not soft) # attempts to find conflict-free renaming order, or exits
if os.path.exists(newRelPath):
exists:str[] = [filename[1] for filename in matches if os.path.exists(os.path.join(newRelPath, filename[1]).replace(SLASH, os.sep))]
if exists and not (force or soft): 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"))
else: os.makedirs(os.path.absdir(newRelPath.replace(SLASH, os.sep)))
if not soft: # perform actual renaming
for (source, target) in matches:
try: shutil.move(os.path.abspath(os.path.join(relPath, source).replace(SLASH, os.sep)), os.path.abspath(os.path.join(newRelPath, target).replace(SLASH, os.sep)))
except Exception as E: 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
m.branches[m.branch].tracked[m.branches[m.branch].tracked.index(pattern)] = newPattern
m.saveBranches()

def parse(root:str, cwd:str):
''' Main operation. Main has already chdir into VCS root folder, cwd is original working directory for add, rm. '''
Expand Down
24 changes: 24 additions & 0 deletions sos/tests.coco
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,30 @@ class Tests(unittest.TestCase):
try: sos.commit("nothing"); _.fail() # should not commit anything, as the file in base folder doesn't match the tracked pattern
except: pass

def testTokenizeGlobPattern(_):
_.assertEqual([], sos.tokenizeGlobPattern(""))
_.assertEqual([sos.GlobBlock(False, "*", 0)], sos.tokenizeGlobPattern("*"))
_.assertEqual([sos.GlobBlock(False, "*", 0), sos.GlobBlock(False, "???", 1)], sos.tokenizeGlobPattern("*???"))
_.assertEqual([sos.GlobBlock(True, "x", 0), sos.GlobBlock(False, "*", 1), sos.GlobBlock(True, "x", 2)], sos.tokenizeGlobPattern("x*x"))
_.assertEqual([sos.GlobBlock(True, "x", 0), sos.GlobBlock(False, "*", 1), sos.GlobBlock(False, "??", 2), sos.GlobBlock(False, "*", 4), sos.GlobBlock(True, "x", 5)], sos.tokenizeGlobPattern("x*??*x"))
_.assertEqual([sos.GlobBlock(False, "?", 0), sos.GlobBlock(True, "abc", 1), sos.GlobBlock(False, "*", 4)], sos.tokenizeGlobPattern("?abc*"))

def testTokenizeGlobPatterns(_):
try: sos.tokenizeGlobPatterns("x*x", "x*"); _.fail() # because number of literal strings differs
except: pass
try: sos.tokenizeGlobPatterns("x*", "x?"); _.fail() # because glob patterns differ
except: pass
try: sos.tokenizeGlobPatterns("x*", "?x"); _.fail() # glob patterns differ, regardless of position
except: pass
sos.tokenizeGlobPatterns("x*", "*x") # succeeds, because glob patterns match (differ only in position)
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"))])

def testComputeSequentialPathSet(_):
os.makedirs(branchFolder(0, 0))
os.makedirs(branchFolder(0, 1))
Expand Down
Loading

0 comments on commit 576c32c

Please sign in to comment.