Skip to content

Commit

Permalink
Rewrite and support non-git directory
Browse files Browse the repository at this point in the history
  • Loading branch information
psjay committed Oct 31, 2017
1 parent 20136b8 commit e07de34
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 109 deletions.
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

Edit your code at local like you do it on remote.

Live sync your local code in Git repository to remote development server via rsync.

## Features

* Sync your code only, no other unnecessary files(including `.git` directory and
files matched in `.gitignore`).
* Aggregating changes within one second.
* Automatically: monitor changes.
* Instantly: Sync just after change
* Incrementally: only sync changed parts.
* Efficiently: aggregate all changes within a specifield interval and respect your git-ignore rules, only sync need files.

## Installation

Expand All @@ -21,16 +20,31 @@ $ pip install LiveCode
Monitor changes and sync diff:

```
$ livecode path/to/local/git-repo [user[:password]@]host:/path/on/remote
$ livecode path/to/local/foo [user[:password]@]host:/path/on/remote/bar
```

Then, all the code in `/path/on/remote/bar` on the **remote** host will be exactly
the same as in the local `foo` directory.

If you just want to sync your code manually, you can put a `-s` argument after the command:

```
$ livecode -s path/to/local/foo [user[:password]@]host:/path/on/remote/bar
```

This will cause a sync, then LiveCode will exit.

And you can specifiy sync interval by setting `-i` option:

```
$ livecode -i 2 path/to/local/foo [user[:password]@]host:/path/on/remote/bar
```

Then, all the code in `/path/on/remote/git-repo` on the **remote** host will be exactly
the same as in the local `git-repo` directory.
Then, LiveCode will sync to remote every 2 seconds if changes happened.

If you just want to sync your code manually, you can plus a `-s` argument after the command:
Show help message:

```
$ livecode -s path/to/local/git-repo [user[:password]@]host:/path/on/remote
$ livecode -h
```

This will cause a sync operation, then LiveCode will exit.
30 changes: 0 additions & 30 deletions livecode/ignore_rule.py

This file was deleted.

91 changes: 65 additions & 26 deletions livecode/main.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,110 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import argparse
import datetime
import logging
import subprocess
import os
import time

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

from livecode import ignore_rule
from livecode.repository import Repository
from livecode.repository import Repository, GitRepository
from livecode.sync import Sync


logger = logging.getLogger(__name__)


class LiveCodeEventHandler(FileSystemEventHandler):

def __init__(self, repo):
self._repo = repo
def __init__(self, sync, repos):
super(LiveCodeEventHandler, self).__init__()

self._sync = sync
self._repos = repos

self._sync.start()

def on_any_event(self, event):
if event.src_path.endswith('.git') or event.src_path.endswith('.git/'):
return
if '.git/' in event.src_path:
return
if ignore_rule.is_git_ignored(event.src_path):
return
print '[%s %s] %s.' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), event.event_type.upper(), event.src_path)
self._repo.sync()
repo = match_repo(event.src_path, self._repos)
file_path = repo.filter_path(event.src_path)
if file_path:
self._sync.add_path(file_path)

def stop_sync(self):
self._sync.stop()

def main():

def load_repos(path):
p1 = subprocess.Popen(('find %s -name .git -type d -prune' % path).split(' '), stdout=subprocess.PIPE)
p2 = subprocess.Popen('xargs -n 1 dirname'.split(' '), stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close()
git_paths = p2.communicate()[0].split(os.linesep)[:-1]
git_paths.sort(key=lambda x: (x, -1 * len(x.split(os.path.sep))))
repos = [GitRepository(p) for p in git_paths]
if path not in git_paths:
repos.append(Repository(path))
return repos


def match_repo(path, candidates):
for repo in candidates:
if path.startswith(os.path.abspath(repo.path)):
return repo
return None


def parse_args():
argparser = argparse.ArgumentParser()
argparser.add_argument('-s', '--sync-only', dest='sync_only',
action='store_true', default=False, help='Do a sync, then exit.')
argparser.add_argument('path', nargs='?', default=os.getcwd(), help='Local Git repository path.')
argparser.add_argument('-i', '--interval', dest='interval', type=int, default=3,
help='Sync interval in seconds, defaults to 3.')
argparser.add_argument('path', nargs='?', default=os.getcwd(), help='Local path to be synced')
argparser.add_argument('remote_path', type=str, help='Remote path.')
args = argparser.parse_args()
path = os.path.abspath(args.path).rstrip('/')
remote_path = args.remote_path.rstrip('/')
return args

def config_logger():
root = logging.getLogger()
root.setLevel(logging.INFO)

if not Repository.validate_path(path):
print 'ERROR: Not a git repository.'
exit(1)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(levelname)s %(asctime)s] %(message)s')
ch.setFormatter(formatter)
root.addHandler(ch)


def main():
config_logger()
args = parse_args()

os.chdir(path)
repo = Repository(path, remote_path)
repos = load_repos(args.path)
sync = Sync(args.path, args.remote_path, interval=args.interval)

print 'Sync on startup.'
repo.sync()
logging.info('Syncing: %s', args.path)
sync.add_path(args.path)
sync.sync(archive_mode=True)

if args.sync_only:
return

# set up monitor
event_handler = LiveCodeEventHandler(repo)
event_handler = LiveCodeEventHandler(sync, repos)
observer = Observer()
observer.schedule(event_handler, path, recursive=True)
observer.schedule(event_handler, os.path.abspath(args.path), recursive=True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
logging.info('LiveCode will exit after the current sync.')
event_handler.stop_sync()
observer.join()


Expand Down
77 changes: 36 additions & 41 deletions livecode/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,47 @@
# -*- coding: utf-8 -*-

import os
import subprocess
import time
import threading

from livecode import ignore_rule
from subprocess import Popen


class Repository(object):

def __init__(self, path, remote_path):
self._path = path
self._remote_path = remote_path
self._last_sync = 0
self._ignore_rules = '\n'.join(self._convert_ignore_file(self._path))
def __init__(self, path):
self.path = path

def filter_path(self, path):
return path


class GitRepository(Repository):

@staticmethod
def validate_path(path):
if os.path.exists(os.path.join(path, '.git')):
def __init__(self, path):
super(GitRepository, self).__init__(path)
self._ignored_files = set([])

def is_ignored(self, relpath):
if relpath.endswith('.git') or '.git/' in relpath:
return True
if relpath.endswith('.gitignore'):
self._ignored_files = set([])
if relpath in self._ignored_files:
return True
if self._check_ignore(relpath):
self._ignored_files.add(relpath)
return True
return False

def _convert_ignore_file(self, repo_path):
if not os.path.exists(os.path.join(repo_path, '.gitignore')):
return ''

with open(os.path.join(repo_path, '.gitignore'), 'r') as ignore_file:
lines = ignore_file.readlines()
return ignore_rule.git2rsync(lines, self._path)

def _do_sync(self):
print 'Start to sync.'
rsync = subprocess.Popen(['rsync', '-av', '--delete',
'--exclude=.git/', '--exclude-from=-',
self._path, self._remote_path],
stdin=subprocess.PIPE)
rsync.communicate(input=self._ignore_rules)

def sync(self):
now = time.time()
if self._last_sync == 0:
self._last_sync = now
self._do_sync()
else:
if now < self._last_sync:
return
else:
self._last_sync = now + 1
t = threading.Timer(1, self._do_sync)
t.start()
def _check_ignore(self, relpath):
cwd = os.getcwd()
try:
os.chdir(self.path)
params = ['git', 'check-ignore', '-q', relpath]
ret = Popen(params).wait()
return ret == 0
finally:
os.chdir(cwd)

def filter_path(self, path):
relpath = os.path.relpath(path, os.path.abspath(self.path))
if not self.is_ignored(relpath):
return path
76 changes: 76 additions & 0 deletions livecode/sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import time
import subprocess
import logging
from threading import RLock, Thread


class SyncRunner(Thread):

def __init__(self, sync, **kwargs):
super(SyncRunner, self).__init__(**kwargs)
self._sync = sync
self._stopped = False

def run(self):
while not self._stopped:
time.sleep(self._sync.interval)
self._sync.sync()

def stop(self):
self._stopped = True


class Sync(object):

def __init__(self, path, remote_path, interval=3):
self._path = path
self._remote_path = remote_path
self.interval = interval

self._abspath = os.path.abspath(self._path)
self._paths_to_sync = set([])
self._lock = RLock()
self._runner = SyncRunner(self)

def sync(self, archive_mode=False):
paths_to_sync = None
with self._lock:
paths_to_sync = self._paths_to_sync
self._paths_to_sync = set([])
if paths_to_sync:
logging.info('Syncing:%s%s', os.linesep, os.linesep.join(
[os.path.relpath(p) for p in paths_to_sync]
))
self._rsync_paths(paths_to_sync, archive_mode)

def _rsync_paths(self, paths, archive_mode=False):
if archive_mode:
params = ['rsync', '-aqzR', '--delete']
else:
params = ['rsync', '-dqR', '--delete']
params.extend([self._cal_path_with_implied_dirs(p) for p in paths])
params.append(self._remote_path)
subprocess.Popen(params).wait()

def _cal_path_with_implied_dirs(self, file_path):
relpath = os.path.relpath(file_path, self._abspath)
return os.path.join(self._path, '.', relpath)

def start(self):
self._runner.start()

def stop(self):
self._runner.stop()
self._runner.join()

def add_path(self, path):
with self._lock:
self._paths_to_sync.add(os.path.abspath(path))

def remove_path(self, path):
with self._lock:
self._paths_to_sync.remove(os.path.abspath(path))
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

setup(
name="LiveCode",
version="0.0.2",
version="0.1.0",
url='https://github.com/psjay/LiveCode',
license='MIT',
description="Live sync your local code in Git repository to remote development server via rsync.",
Expand Down

0 comments on commit e07de34

Please sign in to comment.