Skip to content
42 changes: 27 additions & 15 deletions gitfs/cache/gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,25 @@


class CachedIgnore(object):
def __init__(self, ignore=False, submodules=False, path="."):
def __init__(self, ignore=False, submodules=False, exclude=False,
hard_ignore=None):
self.items = []

self.ignore = False
if ignore:
self.ignore = os.path.join(path, ".gitignore")

self.submodules = False
if submodules:
self.submodules = os.path.join(path, ".gitmodules")
self.ignore = ignore
self.submodules = submodules
self.exclude = exclude

self.cache = {}
self.permanent = []
self.hard_ignore = self._parse_hard_ignore(hard_ignore)

self.update()

def update(self):
self.items = ['.git', '.git/*', '/.git/*', '*.keep']
self.items = ['.git', '.git/*', '/.git/*', '*.keep', '*.gitmodules']

if self.ignore and os.path.exists(self.ignore):
with open(self.ignore) as gitignore:
for item in gitignore.readlines():
item = item.strip()
if item and not item.startswith('#'):
self.items += item
self.items += self._parse_ignore_file(self.ignore)
self.items += self._parse_ignore_file(self.exclude)

if self.submodules and os.path.exists(self.submodules):
with open(self.submodules) as submodules:
Expand All @@ -56,6 +50,24 @@ def update(self):
self.items.append("%s" % result[2])

self.cache = {}
self.items += self.hard_ignore

def _parse_ignore_file(self, ignore_file):
items = []

if ignore_file and os.path.exists(ignore_file):
with open(ignore_file) as gitignore:
for item in gitignore.readlines():
item = item.strip()
if item and not item.startswith('#'):
items.append(item)
return items

def _parse_hard_ignore(self, hard_ignore):
if isinstance(hard_ignore, basestring):
return hard_ignore.split("|")
else:
return []

def __contains__(self, path):
return self.check_key(path)
Expand Down
4 changes: 3 additions & 1 deletion gitfs/mounter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def prepare_components(args):
max_size=args.max_size * 1024 * 1024,
max_offset=args.max_size * 1024 * 1024,
commit_queue=commit_queue,
credentials=credentials)
credentials=credentials,
ignore_file=args.ignore_file,
hard_ignore=args.hard_ignore)

# register all the routes
router.register(routes)
Expand Down
14 changes: 10 additions & 4 deletions gitfs/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@


import re
import inspect
import shutil
import os
import time
import shutil
import inspect

from pwd import getpwnam
from grp import getgrnam
Expand Down Expand Up @@ -59,8 +60,13 @@ def __init__(self, remote_url, repo_path, mount_path, credentials,
log.info('Done cloning')

self.repo.credentials = credentials
self.repo.ignore = CachedIgnore(submodules=True, ignore=True,
path=self.repo_path)

submodules = os.path.join(self.repo_path, '.gitmodules')
ignore = os.path.join(self.repo_path, '.gitignore')
self.repo.ignore = CachedIgnore(submodules=submodules,
ignore=ignore,
exclude=kwargs['ignore_file'] or None,
hard_ignore=kwargs['hard_ignore'])

self.uid = getpwnam(user).pw_uid
self.gid = getgrnam(group).gr_gid
Expand Down
2 changes: 2 additions & 0 deletions gitfs/utils/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def __init__(self, parser):
("log_level", ("warning", "string")),
("cache_size", (800, "int")),
("sentry_dsn", (self.get_sentry_dsn, "string")),
("ignore_file", ("", "string")),
("hard_ignore", ("", "string")),
])
self.config = self.build_config(parser.parse_args())

Expand Down
59 changes: 52 additions & 7 deletions gitfs/views/current.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ def symlink(self, name, target):
log.debug("CurrentView: Created symlink to %s from %s", name, target)
return result

@write_operation
@not_in("ignore", check=["target"])
def link(self, name, target):
if target.startswith('/current/'):
target = target.replace('/current/', '/')

result = super(CurrentView, self).link(target, name)

message = "Create link to %s for %s" % (target, name)
self._stage(add=name, message=message)

log.debug("CurrentView: Created link to %s from %s", name, target)
return result

def readlink(self, path):
log.debug("CurrentView: Read link %s", path)
return os.readlink(self.repo._full_path(path))
Expand Down Expand Up @@ -241,21 +255,52 @@ def unlink(self, path):
def _stage(self, message, add=None, remove=None):
non_empty = False

add = self._sanitize(add)
remove = self._sanitize(remove)

if remove is not None:
self.repo.index.remove(self._sanitize(remove))
remove = self._sanitize(remove)
if add is not None:
add = self._sanitize(add)
paths = self._get_files_from_path(add)
if paths:
for path in paths:
path = path.replace("%s/" % add, "%s/" % remove)
self.repo.index.remove(path)
else:
self.repo.index.remove(remove)
else:
self.repo.index.remove(remove)
non_empty = True

if add is not None:
self.repo.index.add(self._sanitize(add))
add = self._sanitize(add)
paths = self._get_files_from_path(add)
if paths:
for path in paths:
self.repo.index.add(path)
else:
self.repo.index.add(add)
non_empty = True

if non_empty:
self.queue.commit(add=add, remove=remove, message=message)

def _get_files_from_path(self, path):
paths = []

full_path = self.repo._full_path(self._sanitize(path))
workdir = self.repo._repo.workdir

if os.path.isdir(full_path):
for (dirpath, dirs, files) in os.walk(full_path):
for filename in files:
paths.append("%s/%s" % (dirpath.replace(workdir, ''),
filename))
return paths

def _sanitize(self, path):
if path is not None and path.startswith("/"):
path = path[1:]
if path is None:
return path

if path.startswith("/"):
return path[1:]

return path
4 changes: 2 additions & 2 deletions tests/cache/test_gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def test_init(self):
gitignore = CachedIgnore("some_file", "some_file")

assert gitignore.items == ['.git', '.git/*', '/.git/*',
'*.keep', '/found/*',
'/found', 'found']
'*.keep', '*.gitmodules',
'/found/*', '/found', 'found']

def test_update(self):
gitignore = CachedIgnore()
Expand Down
36 changes: 34 additions & 2 deletions tests/integrations/current/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,44 @@
import string
import shutil

import pytest

from tests.integrations.base import BaseTest, pull


class TestWriteCurrentView(BaseTest):
def test_rename_directory(self):
old_dir = "%s/a_directory/" % self.current_path
new_dir = "%s/some_directory/" % self.current_path
os.makedirs(old_dir)

time.sleep(5)
with pull(self.sh):
self.assert_new_commit()

os.rename(old_dir, new_dir)

time.sleep(5)
with pull(self.sh):
self.assert_new_commit()

assert os.path.isdir(new_dir) is not False
assert os.path.exists(old_dir) is False

def test_link_a_file(self):
filename = "%s/link_file" % self.current_path
link_name = "%s/new_link" % self.current_path

with open(filename, "w") as f:
f.write("some content")

os.link(filename, link_name)

time.sleep(5)
with pull(self.sh):
self.assert_commit_message("Update 2 items")

is_link = os.path.isfile(link_name)
assert is_link is not False

def test_write_a_file(self):
content = "Just a small file"
filename = "%s/new_file" % self.current_path
Expand Down
7 changes: 5 additions & 2 deletions tests/test_mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ def test_prepare_components(self):
'merge_timeout': 10,
'commiter_name': 'commit',
'commiter_email': 'commiter@commiting.org',
'log': 'syslog'
'log': 'syslog',
'ignore_file': '',
'module_file': '',
'hard_ignore': None,
})

mocked_argparse.Argumentparser.return_value = mocked_parser
Expand Down Expand Up @@ -91,7 +94,7 @@ def test_prepare_components(self):
'branch': 'branch',
'timeout': 10,
'repo_path': 'repo_path',
'commit_queue': mocked_queue
'commit_queue': mocked_queue,
}
mocked_merger.assert_called_once_with('commit',
'commiter@commiting.org',
Expand Down
10 changes: 7 additions & 3 deletions tests/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ def get_new_router(self):
'commit_queue': mocked_queue,
'max_size': 10,
'max_offset': 10,
'ignore_file': '',
'module_file': '',
'hard_ignore': None,
}

with patch.multiple('gitfs.router', Repository=mocked_repository,
Expand All @@ -92,9 +95,10 @@ def test_constructor(self):
mocks['branch'], mocks['credentials'])
mocks['repository'].clone.assert_called_once_with(*asserted_call)
mocks['ignore'].assert_called_once_with(**{
'submodules': True,
'ignore': True,
'path': mocks['repo_path'],
'ignore': 'repository_path/.gitignore',
'exclude': None,
'hard_ignore': None,
'submodules': 'repository_path/.gitmodules'
})
mocks['getpwnam'].assert_called_once_with(mocks['user'])
mocks['getgrnam'].assert_called_once_with(mocks['group'])
Expand Down
11 changes: 11 additions & 0 deletions tests/views/test_current.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ def test_write_in_git_dir(self):
ignore=CachedIgnore())
current.write(".git/index", "buf", "offset", 1)

def test_write_in_modules_dir(self):
with pytest.raises(FuseOSError):
current = CurrentView(repo="repo", uid=1, gid=1,
repo_path="repo_path",
read_only=Event(),
ignore=CachedIgnore())
current.write(".gitmodules", "buf", "offset", 1)

def test_write_to_large_file(self):
current = CurrentView(repo="repo", uid=1, gid=1,
repo_path="repo_path",
Expand Down Expand Up @@ -360,13 +368,15 @@ def test_stage(self):
mocked_repo = MagicMock()
mocked_sanitize = MagicMock()
mocked_queue = MagicMock()
mocked_files = MagicMock(return_value=None)

mocked_sanitize.return_value = ["to-stage"]

current = CurrentView(repo=mocked_repo,
repo_path="repo_path",
queue=mocked_queue, ignore=CachedIgnore())
current._sanitize = mocked_sanitize
current._get_files_from_path = mocked_files
current._stage("message", ["add"], ["remove"])

mocked_queue.commit.assert_called_once_with(add=['to-stage'],
Expand All @@ -375,6 +385,7 @@ def test_stage(self):
mocked_repo.index.add.assert_called_once_with(["to-stage"])
mocked_repo.index.remove.assert_called_once_with(["to-stage"])

mocked_files.has_calls([call(['add'])])
mocked_sanitize.has_calls([call(['add']), call(['remove'])])

def test_sanitize(self):
Expand Down