diff --git a/Vagrantfile b/Vagrantfile
new file mode 100755
index 0000000..34d5855
--- /dev/null
+++ b/Vagrantfile
@@ -0,0 +1,79 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+# All Vagrant configuration is done below. The "2" in Vagrant.configure
+# configures the configuration version (we support older styles for
+# backwards compatibility). Please don't change it unless you know what
+# you're doing.
+Vagrant.configure(2) do |config|
+  # The most common configuration options are documented and commented below.
+  # For a complete reference, please see the online documentation at
+  # https://docs.vagrantup.com.
+
+  # Every Vagrant development environment requires a box. You can search for
+  # boxes at https://atlas.hashicorp.com/search.
+  config.vm.box = "dharmab/centos6"
+
+  # Disable automatic box update checking. If you disable this, then
+  # boxes will only be checked for updates when the user runs
+  # `vagrant box outdated`. This is not recommended.
+  # config.vm.box_check_update = false
+
+  # Create a forwarded port mapping which allows access to a specific port
+  # within the machine from a port on the host machine. In the example below,
+  # accessing "localhost:8080" will access port 80 on the guest machine.
+  # config.vm.network "forwarded_port", guest: 80, host: 8080
+
+  # Create a private network, which allows host-only access to the machine
+  # using a specific IP.
+  # config.vm.network "private_network", ip: "192.168.33.10"
+
+  # Create a public network, which generally matched to bridged network.
+  # Bridged networks make the machine appear as another physical device on
+  # your network.
+  # config.vm.network "public_network"
+
+  # Share an additional folder to the guest VM. The first argument is
+  # the path on the host to the actual folder. The second argument is
+  # the path on the guest to mount the folder. And the optional third
+  # argument is a set of non-required options.
+  # config.vm.synced_folder "../data", "/vagrant_data"
+
+  # Provider-specific configuration so you can fine-tune various
+  # backing providers for Vagrant. These expose provider-specific options.
+  # Example for VirtualBox:
+  #
+  # config.vm.provider "virtualbox" do |vb|
+  #   # Display the VirtualBox GUI when booting the machine
+  #   vb.gui = true
+  #
+  #   # Customize the amount of memory on the VM:
+  #   vb.memory = "1024"
+  # end
+  #
+  # View the documentation for the provider you are using for more
+  # information on available options.
+
+  # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
+  # such as FTP and Heroku are also available. See the documentation at
+  # https://docs.vagrantup.com/v2/push/atlas.html for more information.
+  # config.push.define "atlas" do |push|
+  #   push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
+  # end
+
+  # Enable provisioning with a shell script. Additional provisioners such as
+  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
+  # documentation for more information about their specific syntax and use.
+   config.vm.provision "shell", inline: <<-SHELL
+     # create venv
+     sudo yum -y install python-virtualenv python-pip
+     sudo yum -y install python-virtualenvwrapper
+     mkvirtualenv --no-site-packages vcs   # doesn't work
+
+     # install perforce
+     sudo rpm --import https://package.perforce.com/perforce.pubkey
+     sudo yum install yum-utils
+     sudo yum-config-manager --add-repo=http://package.perforce.com/yum/rhel/6/x86_64
+     sudo yum install helix-cli
+   SHELL
+end
diff --git a/docs/api/backends/p4.rst b/docs/api/backends/p4.rst
new file mode 100755
index 0000000..eb630c7
--- /dev/null
+++ b/docs/api/backends/p4.rst
@@ -0,0 +1,21 @@
+.. _api-backends-p4:
+
+vcs.backends.p4
+===============
+
+.. automodule:: vcs.backends.p4
+
+P4Repository
+-------------------
+
+.. autoclass:: vcs.backends.p4.P4Repository
+   :members:
+
+P4Changeset
+------------------
+
+.. autoclass:: vcs.backends.p4.P4Changeset
+   :members:
+   :inherited-members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/setup.py b/setup.py
old mode 100644
new mode 100755
index 44e9f95..e660fe3
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@
         "long_description (%s)\n" % readme_file)
     sys.exit(1)
 
-install_requires = ['Pygments']
+install_requires = ['Pygments', 'pytz']
 
 if sys.version_info < (2, 7):
     install_requires.append('unittest2')
diff --git a/vcs/backends/p4/__init__.py b/vcs/backends/p4/__init__.py
new file mode 100755
index 0000000..70df4d1
--- /dev/null
+++ b/vcs/backends/p4/__init__.py
@@ -0,0 +1,16 @@
+"""
+Implements perforce backend. Uses command p4 -G to get all the info.
+
+Perforce streams not supported yet. Helix dvcs not supported yet.
+
+Used with server and client 2015.1. Older or newer versions may not work.
+"""
+
+from .repository import P4Repository
+from .changeset import P4Changeset
+
+
+
+__all__ = [
+    'P4Repository', 'P4Changeset'
+]
diff --git a/vcs/backends/p4/changeset.py b/vcs/backends/p4/changeset.py
new file mode 100755
index 0000000..e700850
--- /dev/null
+++ b/vcs/backends/p4/changeset.py
@@ -0,0 +1,276 @@
+import datetime
+import logging
+
+import pytz
+
+from vcs.backends.base import BaseChangeset
+from vcs.utils.lazy import LazyProperty
+
+class P4Changeset(BaseChangeset):
+    """
+    P4 changelist. Submitted CLs will be implemented first.
+
+    **Attributes**
+
+        ``repository``
+            repository object within which changeset exists
+
+        ``id``
+            Changelist number (int)
+
+        ``raw_id``
+            same as id
+
+        ``short_id``
+            same as id
+
+        ``revision``
+            same as id
+
+        ``files``
+            list of ``FileNode`` (``Node`` with NodeKind.FILE) objects, TBD
+
+        ``dirs``
+            list of ``DirNode`` (``Node`` with NodeKind.DIR) objects, TBD
+
+        ``nodes``
+            combined list of ``Node`` objects, TBD
+
+        ``author``
+            author of the changeset, as unicode, TBD
+
+        ``message``
+            message of the changeset, as unicode
+
+        ``parents``
+            list of parent changesets, TBD
+
+        ``last``
+            ``True`` if this is last changeset in repository, ``False``
+            otherwise; trying to access this attribute while there is no
+            changesets would raise ``EmptyRepositoryError``, TBD
+
+        ``date``
+            datetime object representing date and time of the submit. TZ aware.
+
+        Added properties:
+
+        ``raw_data``
+            the raw dict returned by p4 lib or cmd
+    """
+    def __init__(self, repository, changeset_dict):
+        """
+        :param repository: repository object
+        :param changeset_dict: the raw dict returned by p4 cmd or lib
+        :return:
+        """
+        self.repository = repository
+        self.revision = int(changeset_dict['change'])
+        self.short_id = self.revision
+        self.id = self.revision
+
+        self.author = changeset_dict['user']
+        self.message = changeset_dict['desc']
+
+        self.raw_data = changeset_dict
+        self.date = datetime.datetime.utcfromtimestamp(int(changeset_dict['time']))  # it is really in utc
+        self.date.replace(tzinfo=pytz.UTC)
+        self._describe_result = None
+
+    @LazyProperty
+    def parents(self):
+        """
+        Returns list of parents changesets.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def children(self):
+        """
+        Returns list of children changesets.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def id(self):
+        """
+        Returns string identifying this changeset.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def raw_id(self):
+        """
+        Returns raw string identifying this changeset.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def short_id(self):
+        """
+        Returns shortened version of ``raw_id`` attribute, as string,
+        identifying this changeset, useful for web representation.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def revision(self):
+        """
+        Returns integer identifying this changeset.
+
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def committer(self):
+        """
+        Returns Committer for given commit
+        """
+
+        raise NotImplementedError
+
+    @LazyProperty
+    def committer_name(self):
+        """
+        Returns Author name for given commit
+        """
+
+        return author_name(self.committer)
+
+    @LazyProperty
+    def committer_email(self):
+        """
+        Returns Author email address for given commit
+        """
+
+        return author_email(self.committer)
+
+    @LazyProperty
+    def author(self):
+        """
+        Returns Author for given commit
+        """
+
+        raise NotImplementedError
+
+
+    def get_file_mode(self, path):
+        """
+        Returns stat mode of the file at the given ``path``.
+        """
+        raise NotImplementedError
+
+    def get_file_content(self, path):
+        """
+        Returns content of the file at the given ``path``.
+        """
+        raise NotImplementedError
+
+    def get_file_size(self, path):
+        """
+        Returns size of the file at the given ``path``.
+        """
+        raise NotImplementedError
+
+    def get_file_changeset(self, path):
+        """
+        Returns last commit of the file at the given ``path``.
+        """
+        raise NotImplementedError
+
+    def get_file_history(self, path):
+        """
+        Returns history of file as reversed list of ``Changeset`` objects for
+        which file at given ``path`` has been modified.
+        """
+        raise NotImplementedError
+
+    def get_nodes(self, path):
+        """
+        Returns combined ``DirNode`` and ``FileNode`` objects list representing
+        state of changeset at the given ``path``.
+
+        :raises ``ChangesetError``: if node at the given ``path`` is not
+          instance of ``DirNode``
+        """
+        raise NotImplementedError
+
+    def get_node(self, path):
+        """
+        Returns ``Node`` object from the given ``path``.
+
+        :raises ``NodeDoesNotExistError``: if there is no node at the given
+          ``path``
+        """
+        raise NotImplementedError
+
+    def fill_archive(self, stream=None, kind='tgz', prefix=None):
+        """
+        Fills up given stream.
+
+        :param stream: file like object.
+        :param kind: one of following: ``zip``, ``tar``, ``tgz``
+            or ``tbz2``. Default: ``tgz``.
+        :param prefix: name of root directory in archive.
+            Default is repository name and changeset's raw_id joined with dash.
+
+            repo-tip.<kind>
+        """
+
+        raise NotImplementedError
+
+    def next(self, branch=None):
+        """
+        Returns next changeset from current, if branch is gives it will return
+        next changeset belonging to this branch
+
+        :param branch: show changesets within the given named branch
+        """
+        raise NotImplementedError
+
+    def prev(self, branch=None):
+        """
+        Returns previous changeset from current, if branch is gives it will
+        return previous changeset belonging to this branch
+
+        :param branch: show changesets within the given named branch
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def added(self):
+        """
+        Returns list of added ``FileNode`` objects.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def changed(self):
+        """
+        Returns list of modified ``FileNode`` objects.
+        """
+        raise NotImplementedError
+
+    @LazyProperty
+    def removed(self):
+        """
+        Returns list of removed ``FileNode`` objects.
+        """
+        raise NotImplementedError
+
+    def _describe(self):
+        if not self._describe_result:
+            self._describe_result = self.repository.repo.run(['describe', self.id])
+
+        if not len(self._describe_result) == 1:
+            logging.warning('describe returned something unexpected: %s', self._describe_result)
+
+        return self._describe_result
+
+    def affected_files(self):
+        try:
+            return self._describe()[-1]['depotFile']
+        except:
+            logging.warning('No files in that changest %d', self.id)
+            return []
+
diff --git a/vcs/backends/p4/common.py b/vcs/backends/p4/common.py
new file mode 100755
index 0000000..3e49a32
--- /dev/null
+++ b/vcs/backends/p4/common.py
@@ -0,0 +1,152 @@
+import abc
+import logging
+import subprocess
+import marshal
+
+import re
+
+import vcs.exceptions
+
+logger = logging.getLogger(__name__)
+
+
+class P4(object):
+    __metaclass__ = abc.ABCMeta
+
+    def __init__(self, user, passwd, port, client, env=None):
+        self.user = user
+        self.client = passwd
+        self.port = port
+        self.client = client
+        self.env = env
+
+    @abc.abstractmethod
+    def run(self, args, input=None):
+        """
+
+        :param args: list of arguments, e.g. ['foo', 'bar']
+        :param input:
+        :return:
+        """
+        pass
+
+
+class SubprocessP4(P4):
+    """A command-line based P4 class.
+
+    Based on using 'p4 -G ...', which returns marshaled
+    python dictionaries as its output values, together with a 'code' key which may be 'stat', 'error', or 'info'.
+    The normal return case is 'stat'. Some fixup is required for things like array value results.
+    """
+    ARRAY_KEY = re.compile(r'(\w+?)(\d+)$')
+    INT = re.compile(r'\d+$')
+
+    def run(self, args, input=None, env=None):
+        logger.debug('Going to run p4 command %s', str(args))
+        stdin_mode = subprocess.PIPE if input is not None else None
+
+        p = subprocess.Popen(['p4', '-G'] + map(str, args), stdin=stdin_mode, stdout=subprocess.PIPE,
+                             env=env or self.env)
+
+        if input is not None:
+            input = SubprocessP4.encode_arrays(input)
+            marshal.dump(input, p.stdin, 0)  # must specify version 0; http://kb.perforce.com/article/585/using-p4-g
+            p.stdin.close()
+
+        result = []
+
+        while True:
+            try:
+                h = marshal.load(p.stdout)
+                if not isinstance(h, dict):
+                    raise vcs.exceptions.CommandError('Command %r produced unmarshalled object of the wrong type: %r',
+                                    (['p4', '-G'] + map(str, args), h))
+                result.append(h)
+            except EOFError:
+                break
+
+        return SubprocessP4.post_process(result, args)
+
+    @staticmethod
+    def post_process(result, args):
+        def append(l, v):
+            l.append(v)
+
+        def process_dict(h):
+            code = h.get('code', None)
+            if code == 'error':
+                name = 'Error' if h['severity'] == 3 else 'Warning'
+                raise vcs.exceptions.CommandError('%ss during command execution( "p4 -G %s" )\n\n\t[%s]: %s\n' %
+                                                  (name, ' '.join(map(str, args)), name, h['data']))
+            elif code == 'info':
+                return h['data'], append
+            elif code == 'text':
+                # Mimic behavior of 'diff',
+                # e.g.: Result is a dict of clientFIle, depotFile, rev, text, then lines.
+                return h['data'], lambda l, v: l.extend(v.splitlines())
+            elif code and code != 'stat':
+                raise vcs.exceptions.CommandError('ERROR: %r => %r' % (args, h))  # never happens
+            if code:
+                del h['code']
+
+            SubprocessP4.decode_arrays(h)
+            return h, append
+
+        a = []
+        for value, fn in map(process_dict, result):
+            fn(a, value)
+        return a
+
+    @staticmethod
+    def encode_arrays(h):
+        if not isinstance(h, dict):
+            return h
+        keys_with_lists = [(key, value) for key, value in h.items()
+                         if isinstance(value, (list, tuple))]
+        if not keys_with_lists:
+            return h
+        for key, valueList in keys_with_lists:
+            for i, value in enumerate(valueList):
+                indexed_key = '%s%d' % (key, i)
+                assert indexed_key not in h, 'Key %r already present in pre-marshaled dict: %r' % (indexed_key, h)
+                h[indexed_key] = value
+            del h[key]
+        return h
+
+    @staticmethod
+    def decode_arrays(h):
+        """If there is an array of values, e.g. the result of [ p4 -G describe CHANGELIST ], then they are returned
+        as key0: value0, key1: value1, etc. Convert these to key: [value0, value1, ...]."""
+        keys_and_indices = [(m.group(1), int(m.group(2))) for key, m in
+                          [(key, SubprocessP4.ARRAY_KEY.match(key)) for key in h.keys()]
+                          if m]
+        if not keys_and_indices:
+            return h
+        key_range = {}
+        for key, index in keys_and_indices:
+            index_range = key_range.setdefault(key, [None, None])
+            index_range[0] = index if index_range[0] is None else min(index_range[0], index)
+            index_range[1] = index if index_range[1] is None else max(index_range[1], index)
+        for key, index_range in key_range.items():
+            # Handle otherOpen, otherChange, otherLock
+            if re.match(r'other[A-Z][a-z]*[a-rt-z]$', key):
+                plural_key = key + 's'
+                # fix special case (fstat)
+                # n.b.: sometimes the key value is '' instead of the array length (bug in p4???)
+                if key in h and plural_key not in h and (SubprocessP4.INT.match(h[key]) or h[key] == ''):
+                    h[plural_key] = h[key]
+                    del h[key]
+            assert key not in h, 'Key %r already present in unmarshaled dict: %r' % (key, h)
+            a = []
+            for i in range(index_range[1]+1):
+                indexed_key = '%s%d' % (key, i)
+                if indexed_key in h:
+                    a.append(h[indexed_key])
+                    del h[indexed_key]
+                else:
+                    a.append(None)  # n.b.: if zeroth entry is missing, p4 puts a None in
+            h[key] = a
+        return h
+
+def get_p4_class():
+    return SubprocessP4
\ No newline at end of file
diff --git a/vcs/backends/p4/repository.py b/vcs/backends/p4/repository.py
new file mode 100755
index 0000000..f3b74f7
--- /dev/null
+++ b/vcs/backends/p4/repository.py
@@ -0,0 +1,275 @@
+import os
+
+import datetime
+
+from vcs.backends.base import BaseRepository
+from .common import get_p4_class
+from .changeset import P4Changeset
+import vcs.exceptions
+from vcs.utils.lazy import LazyProperty
+
+class P4Repository(BaseRepository):
+    """
+    Base Repository for final backends
+
+        ``repo``
+            object from external api
+
+        ``revisions``
+            list of all available revisions' ids, in ascending order
+
+        ``changesets``
+            storage dict caching returned changesets
+
+        ``path``
+            absolute path to the repository
+
+        ``branches``
+            branches as list of changesets
+
+        ``tags``
+            tags as list of changesets
+    """
+    scm = 'p4'
+    DEFAULT_BRANCH_NAME = None
+
+    def __init__(self, repo_path, create=False, user=None, passwd=None, port=None, p4client=None):
+        """
+        Initializes repository. Raises RepositoryError if repository could
+        not be find at the given ``repo_path`` or directory at ``repo_path``
+        exists and ``create`` is set to True.
+
+        :param repo_path: e.g. //depot/path/to/dir
+        :param create=False: if set to True, would try to sync to your workspace
+        :param p4user=None: Username for authorization on p4 server. If None, taken from env var P4USER
+        :param p4passwd=None: Password for authorization on p4 server. If None, taken from env var P4PASSWD
+        :param p4port=None Protocol, host and port of the p4 server,
+         e.g. ssl:perforce.mycompany.com:1667 If None, taken from env var P4PORT
+        :param p4client=None name of workspace to use for sync/write operations.
+         If not specified, taken from env var P4CLIENT. Not used yet.
+        """
+        self.path = repo_path
+
+        try:
+            user = user or os.environ['P4USER']
+            passwd = passwd or os.environ['P4PASSWD']
+            port = port or os.environ['P4PORT']
+        except KeyError:
+            raise vcs.exceptions.RepositoryError('You have to specify user, password and port')
+
+        client = p4client or os.environ.get('P4CLIENT')  # this one isn't mandatory for read operations
+
+        self.repo_path = repo_path
+        p4class = get_p4_class()
+        self.repo = p4class(user, passwd, port, client)
+
+    def is_valid(self):
+        """
+        Validates repository.
+        """
+        raise NotImplementedError
+
+    #==========================================================================
+    # CHANGESETS
+    #==========================================================================
+
+    def get_changeset(self, revision=None):
+        """
+        Returns instance of ``Changeset`` class. If ``revision`` is None, most
+        recent changeset is returned.
+
+        :raises ``EmptyRepositoryError``: if there are no revisions
+        """
+        if revision:
+            changesets = self.get_changesets(start=revision, end=revision)
+
+            if not changesets:
+                raise vcs.exceptions.ChangesetDoesNotExistError('Changeset %d does not exit' % revision)
+
+            return changesets[0]
+        else:
+            raise NotImplementedError('please specify the changeset id')
+
+    def get_changesets(self, start=None, end=None, start_date=None,
+                       end_date=None, branch_name=None, reverse=False):
+        """
+        Returns iterator of ``P4Changeset`` objects from start to end
+        not inclusive This should behave just like a list, ie. end is not
+        inclusive
+
+        :param start: None or int, TBD
+        :param end: None or int, should be bigger than start, TBD
+        :param start_date: instance of datetime or None
+        :param end_date: instance of datetime or None
+        :param branch_name: TBD
+        :param reversed: TBD
+        """
+        # TODO
+        # do the previous command in cycle with modified start and end till you satisfy the start and end.
+        # it is better to have multiple requests with 1000 results than one with millions of results
+
+        # http://stackoverflow.com/questions/17702785/python-generator-for-paged-api-resource
+
+        STR_FORMAT = '%Y/%m/%d %H:%M:%S'
+
+        if start and start_date:
+            raise vcs.exceptions.RepositoryError('both start and start_date specified')
+
+        if start:
+            assert isinstance(start, int)
+            start = str(start)
+        elif start_date:
+            start = start_date.strftime(STR_FORMAT)
+        else:
+            start = 0
+
+        if end and end_date:
+            raise vcs.exceptions.RepositoryError('both end and end_date specified')
+
+        if end:
+            assert isinstance(end, int)
+            end = str(end)
+        elif end_date:
+            end = end_date.strftime(STR_FORMAT)
+        else:
+            end = 'now'
+
+        path_with_revspec = '{path}@{start},{end}'.format(path=self.repo_path, start=start, end=end)
+
+        result = self.repo.run(['changes', '-l', '-s', 'submitted', path_with_revspec])
+        result = [P4Changeset(self, cs) for cs in result]
+
+        return result
+
+    def tag(self, name, user, revision=None, message=None, date=None, **opts):
+        """
+        Creates and returns a tag for the given ``revision``.
+
+        :param name: name for new tag
+        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
+        :param revision: changeset id for which new tag would be created
+        :param message: message of the tag's commit
+        :param date: date of tag's commit
+
+        :raises TagAlreadyExistError: if tag with same name already exists
+        """
+        raise NotImplementedError
+
+    def remove_tag(self, name, user, message=None, date=None):
+        """
+        Removes tag with the given ``name``.
+
+        :param name: name of the tag to be removed
+        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
+        :param message: message of the tag's removal commit
+        :param date: date of tag's removal commit
+
+        :raises TagDoesNotExistError: if tag with given name does not exists
+        """
+        raise NotImplementedError
+
+    def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
+            context=3):
+        """
+        Returns (git like) *diff*, as plain text. Shows changes introduced by
+        ``rev2`` since ``rev1``.
+
+        :param rev1: Entry point from which diff is shown. Can be
+          ``self.EMPTY_CHANGESET`` - in this case, patch showing all
+          the changes since empty state of the repository until ``rev2``
+        :param rev2: Until which revision changes should be shown.
+        :param ignore_whitespace: If set to ``True``, would not show whitespace
+          changes. Defaults to ``False``.
+        :param context: How many lines before/after changed lines should be
+          shown. Defaults to ``3``.
+        """
+        raise NotImplementedError
+
+    # ========== #
+    # COMMIT API #
+    # ========== #
+
+    @LazyProperty
+    def in_memory_changeset(self):
+        """
+        Returns ``InMemoryChangeset`` object for this repository.
+        """
+        raise NotImplementedError
+
+    def add(self, filenode, **kwargs):
+        """
+        Commit api function that will add given ``FileNode`` into this
+        repository.
+
+        :raises ``NodeAlreadyExistsError``: if there is a file with same path
+          already in repository
+        :raises ``NodeAlreadyAddedError``: if given node is already marked as
+          *added*
+        """
+        raise NotImplementedError
+
+    def remove(self, filenode, **kwargs):
+        """
+        Commit api function that will remove given ``FileNode`` into this
+        repository.
+
+        :raises ``EmptyRepositoryError``: if there are no changesets yet
+        :raises ``NodeDoesNotExistError``: if there is no file with given path
+        """
+        raise NotImplementedError
+
+    def commit(self, message, **kwargs):
+        """
+        Persists current changes made on this repository and returns newly
+        created changeset.
+
+        :raises ``NothingChangedError``: if no changes has been made
+        """
+        raise NotImplementedError
+
+    def get_state(self):
+        """
+        Returns dictionary with ``added``, ``changed`` and ``removed`` lists
+        containing ``FileNode`` objects.
+        """
+        raise NotImplementedError
+
+    def get_config_value(self, section, name, config_file=None):
+        """
+        Returns configuration value for a given [``section``] and ``name``.
+
+        :param section: Section we want to retrieve value from
+        :param name: Name of configuration we want to retrieve
+        :param config_file: A path to file which should be used to retrieve
+          configuration from (might also be a list of file paths)
+        """
+        raise NotImplementedError
+
+    def get_user_name(self, config_file=None):
+        """
+        Returns user's name from global configuration file.
+
+        :param config_file: A path to file which should be used to retrieve
+          configuration from (might also be a list of file paths)
+        """
+        raise NotImplementedError
+
+    def get_user_email(self, config_file=None):
+        """
+        Returns user's email from global configuration file.
+
+        :param config_file: A path to file which should be used to retrieve
+          configuration from (might also be a list of file paths)
+        """
+        raise NotImplementedError
+
+    # =========== #
+    # WORKDIR API #
+    # =========== #
+
+    @LazyProperty
+    def workdir(self):
+        """
+        Returns ``Workdir`` instance for this repository.
+        """
+        raise NotImplementedError
\ No newline at end of file
diff --git a/vcs/conf/settings.py b/vcs/conf/settings.py
index d3271f4..eaca971 100644
--- a/vcs/conf/settings.py
+++ b/vcs/conf/settings.py
@@ -27,6 +27,7 @@
 BACKENDS = {
     'hg': 'vcs.backends.hg.MercurialRepository',
     'git': 'vcs.backends.git.GitRepository',
+    'p4':  'vcs.backends.p4.P4Repository'
 }
 
 ARCHIVE_SPECS = {
diff --git a/vcs/tests/__init__.py b/vcs/tests/__init__.py
index 5158bcf..10bd0cb 100644
--- a/vcs/tests/__init__.py
+++ b/vcs/tests/__init__.py
@@ -33,6 +33,7 @@
 from test_getitem import *
 from test_getslice import *
 from test_git import *
+from test_p4 import *
 from test_hg import *
 from test_inmemchangesets import *
 from test_nodes import *
diff --git a/vcs/tests/test_p4.py b/vcs/tests/test_p4.py
new file mode 100755
index 0000000..1e22360
--- /dev/null
+++ b/vcs/tests/test_p4.py
@@ -0,0 +1,57 @@
+import unittest
+import logging
+
+import datetime
+
+from vcs.backends.p4.repository import P4Repository
+
+if True:  # for msamia's purposes
+	TEST_P4_REPO = '//depot/Tools/p4sandbox/...'
+else:
+	P4USER = 'vcs'
+	P4PORT = 'workshop.perforce.com:1666'
+	# see https://swarm.workshop.perforce.com/
+
+
+
+class P4RepositoryTest(unittest.TestCase):
+	"""
+	These tests work only in Michel's repository, they should be changed so they first create some commits on
+	a sandbox server and then query these commits.
+	"""
+
+	def setUp(self):
+		self.repo = P4Repository(TEST_P4_REPO)
+		logging.basicConfig(level=logging.DEBUG)
+
+	def test_get_changelists_end_date_only(self):
+		CLs = self.repo.get_changesets(end_date=datetime.datetime(2014, 1, 1))
+
+		for cl in CLs:
+			logging.debug('%s, %s', cl, cl.date)
+
+		self.assertEqual(len(CLs), 5)
+
+	def test_get_changelists_range(self):
+		CLs = self.repo.get_changesets(start_date=datetime.datetime(2013, 5, 3, 3),
+									   end_date=datetime.datetime(2014, 1, 1))
+
+		for cl in CLs:
+			logging.debug('%s, %s', cl, cl.date)
+
+		self.assertEqual(len(CLs), 2)
+
+	def test_get_affected_files(self):
+		cs = self.repo.get_changeset(562108)
+		files = cs.affected_files()
+		self.assertEqual(len(files), 1)
+		self.assertEqual(files[0], '//depot/Tools/p4sandbox/file_to_be_added.txt')
+
+		# changeset after obliterated files
+		repo = P4Repository('//depot/...')
+		cs = repo.get_changeset(24754)
+		files = cs.affected_files()
+
+
+if __name__ == '__main__':
+	unittest.main()