From 32a97e82a44b65e130dc323d3a550da381388acc Mon Sep 17 00:00:00 2001 From: msamia Date: Tue, 5 Jan 2016 20:27:29 +0100 Subject: [PATCH 01/14] work started. No tests yet --- docs/api/backends/p4.rst | 21 +++ vcs/backends/p4/__init__.py | 14 ++ vcs/backends/p4/changeset.py | 235 ++++++++++++++++++++++++++++++++++ vcs/backends/p4/repository.py | 216 +++++++++++++++++++++++++++++++ 4 files changed, 486 insertions(+) create mode 100755 docs/api/backends/p4.rst create mode 100755 vcs/backends/p4/__init__.py create mode 100755 vcs/backends/p4/changeset.py create mode 100755 vcs/backends/p4/repository.py 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/vcs/backends/p4/__init__.py b/vcs/backends/p4/__init__.py new file mode 100755 index 0000000..08fb7eb --- /dev/null +++ b/vcs/backends/p4/__init__.py @@ -0,0 +1,14 @@ +""" +Implements perforce backend. Uses command p4 -G to get all the info. + +Perforce streams not supported yet. Helix dvcs not supported yet. +""" + +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..4d7a283 --- /dev/null +++ b/vcs/backends/p4/changeset.py @@ -0,0 +1,235 @@ +from vcs.backends.base import BaseChangeset + +class P4Changeset(BaseChangeset): + """ + P4 changelist. Submitted CLs will be implemented first. + + **Attributes** + + ``repository`` + repository object within which changeset exists + + ``id`` + may be ``raw_id`` or i.e. for mercurial's tip just ``tip`` + + ``raw_id`` + raw changeset representation (i.e. full 40 length sha for git + backend) + + ``short_id`` + shortened (if apply) version of ``raw_id``; it would be simple + shortcut for ``raw_id[:12]`` for git/mercurial backends or same + as ``raw_id`` for subversion + + ``revision`` + revision number as integer + + ``files`` + list of ``FileNode`` (``Node`` with NodeKind.FILE) objects + + ``dirs`` + list of ``DirNode`` (``Node`` with NodeKind.DIR) objects + + ``nodes`` + combined list of ``Node`` objects + + ``author`` + author of the changeset, as unicode + + ``message`` + message of the changeset, as unicode + + ``parents`` + list of parent changesets + + ``last`` + ``True`` if this is last changeset in repository, ``False`` + otherwise; trying to access this attribute while there is no + changesets would raise ``EmptyRepositoryError`` + """ + @LazyProperty + def last(self): + if self.repository is None: + raise ChangesetError("Cannot check if it's most recent revision") + return self.raw_id == self.repository.revisions[-1] + + @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. + """ + + 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 diff --git a/vcs/backends/p4/repository.py b/vcs/backends/p4/repository.py new file mode 100755 index 0000000..8dc1788 --- /dev/null +++ b/vcs/backends/p4/repository.py @@ -0,0 +1,216 @@ +import os + +from vcs.backends.base import BaseRepository + +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 + EMPTY_CHANGESET = '0' * 40 + + def __init__(self, repo_path, create=False, p4user=None, p4passwd=None, p4port=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: if set, used for authorization. If none, taken from env var + :param p4passwd=None: same as p4user + :param p4port=None same as p4user + :param p4client=None same as p4port + """ + self.path = repo_path + self.p4user = p4user or os.environ['P4USER'] + self.p4passwd = p4passwd or os.environ['P4PASSWD'] + self.p4port = p4port or os.environ['P4PORT'] + self.p4client = p4client or os.environ['P4CLIENT'] + + 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 + """ + raise NotImplementedError + + def get_changesets(self, start=None, end=None, start_date=None, + end_date=None, branch_name=None, reverse=False): + """ + Returns iterator of ``MercurialChangeset`` objects from start to end + not inclusive This should behave just like a list, ie. end is not + inclusive + + :param start: None or str + :param end: None or str + :param start_date: + :param end_date: + :param branch_name: + :param reversed: + """ + raise NotImplementedError + + 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 " + :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 " + :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 From 67cd6fbf380fa411d96b71b5908137dc93e53d75 Mon Sep 17 00:00:00 2001 From: msamia Date: Wed, 6 Jan 2016 14:44:05 +0100 Subject: [PATCH 02/14] parsing of p4 -G output, started to implement get_changesets --- vcs/backends/p4/__init__.py | 2 + vcs/backends/p4/changeset.py | 5 -- vcs/backends/p4/common.py | 149 ++++++++++++++++++++++++++++++++++ vcs/backends/p4/repository.py | 34 +++++--- 4 files changed, 175 insertions(+), 15 deletions(-) create mode 100755 vcs/backends/p4/common.py diff --git a/vcs/backends/p4/__init__.py b/vcs/backends/p4/__init__.py index 08fb7eb..70df4d1 100755 --- a/vcs/backends/p4/__init__.py +++ b/vcs/backends/p4/__init__.py @@ -2,6 +2,8 @@ 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 diff --git a/vcs/backends/p4/changeset.py b/vcs/backends/p4/changeset.py index 4d7a283..edc8f8f 100755 --- a/vcs/backends/p4/changeset.py +++ b/vcs/backends/p4/changeset.py @@ -47,11 +47,6 @@ class P4Changeset(BaseChangeset): otherwise; trying to access this attribute while there is no changesets would raise ``EmptyRepositoryError`` """ - @LazyProperty - def last(self): - if self.repository is None: - raise ChangesetError("Cannot check if it's most recent revision") - return self.raw_id == self.repository.revisions[-1] @LazyProperty def parents(self): diff --git a/vcs/backends/p4/common.py b/vcs/backends/p4/common.py new file mode 100755 index 0000000..6e05267 --- /dev/null +++ b/vcs/backends/p4/common.py @@ -0,0 +1,149 @@ +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: + :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 diff --git a/vcs/backends/p4/repository.py b/vcs/backends/p4/repository.py index 8dc1788..683d63d 100755 --- a/vcs/backends/p4/repository.py +++ b/vcs/backends/p4/repository.py @@ -1,6 +1,8 @@ import os from vcs.backends.base import BaseRepository +from .common import SubprocessP4 +import vcs.exceptions class P4Repository(BaseRepository): """ @@ -26,9 +28,8 @@ class P4Repository(BaseRepository): """ scm = 'p4' DEFAULT_BRANCH_NAME = None - EMPTY_CHANGESET = '0' * 40 - def __init__(self, repo_path, create=False, p4user=None, p4passwd=None, p4port=None, p4client=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`` @@ -42,10 +43,18 @@ def __init__(self, repo_path, create=False, p4user=None, p4passwd=None, p4port=N :param p4client=None same as p4port """ self.path = repo_path - self.p4user = p4user or os.environ['P4USER'] - self.p4passwd = p4passwd or os.environ['P4PASSWD'] - self.p4port = p4port or os.environ['P4PORT'] - self.p4client = p4client or os.environ['P4CLIENT'] + + 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 + self.repo = SubprocessP4(user, passwd, port, client) def is_valid(self): """ @@ -69,18 +78,23 @@ def get_changeset(self, revision=None): def get_changesets(self, start=None, end=None, start_date=None, end_date=None, branch_name=None, reverse=False): """ - Returns iterator of ``MercurialChangeset`` objects from start to end + 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 str - :param end: None or str + :param start: None or int + :param end: None or int, should be bigger than start :param start_date: :param end_date: :param branch_name: :param reversed: """ - raise NotImplementedError + PAGE_SIZE = 1000 + + result = self.repo.run(['changes', '-s', 'submitted', '-m', PAGE_SIZE, self.repo_path]) + + # 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 1k results, than one with millions of results def tag(self, name, user, revision=None, message=None, date=None, **opts): """ From 4334a9e9306a9180726d6f79638c46af63e091c0 Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Thu, 7 Jan 2016 04:29:36 -0800 Subject: [PATCH 03/14] get_changelists works with limitations --- vcs/backends/p4/changeset.py | 9 ++++++++ vcs/backends/p4/repository.py | 40 ++++++++++++++++++++++++++--------- vcs/tests/__init__.py | 1 + vcs/tests/test_p4.py | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 vcs/tests/test_p4.py diff --git a/vcs/backends/p4/changeset.py b/vcs/backends/p4/changeset.py index edc8f8f..f57566a 100755 --- a/vcs/backends/p4/changeset.py +++ b/vcs/backends/p4/changeset.py @@ -1,4 +1,7 @@ +import datetime + from vcs.backends.base import BaseChangeset +from vcs.utils.lazy import LazyProperty class P4Changeset(BaseChangeset): """ @@ -47,6 +50,12 @@ class P4Changeset(BaseChangeset): otherwise; trying to access this attribute while there is no changesets would raise ``EmptyRepositoryError`` """ + def __init__(self, changeset_dict): + self.revision = int(changeset_dict['change']) + self.short_id = self.revision + self.id = self.revision + self.time = datetime.datetime.utcfromtimestamp(int(changeset_dict['time'])) + self.raw_data = changeset_dict @LazyProperty def parents(self): diff --git a/vcs/backends/p4/repository.py b/vcs/backends/p4/repository.py index 683d63d..ef0b2ab 100755 --- a/vcs/backends/p4/repository.py +++ b/vcs/backends/p4/repository.py @@ -1,8 +1,12 @@ import os +import datetime + from vcs.backends.base import BaseRepository from .common import SubprocessP4 +from .changeset import P4Changeset import vcs.exceptions +from vcs.utils.lazy import LazyProperty class P4Repository(BaseRepository): """ @@ -82,19 +86,35 @@ def get_changesets(self, start=None, end=None, start_date=None, not inclusive This should behave just like a list, ie. end is not inclusive - :param start: None or int - :param end: None or int, should be bigger than start - :param start_date: - :param end_date: - :param branch_name: - :param reversed: + :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 """ - PAGE_SIZE = 1000 + # 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 not start_date: + start_date = datetime.datetime.utcfromtimestamp(0) # january 1970 + + if not end_date: + end_date = datetime.datetime.utcnow() + + path_with_revspec = '{path}@{start_date},{end_date}'.format(path=self.repo_path, + start_date=start_date.strftime(STR_FORMAT), + end_date=end_date.strftime(STR_FORMAT)) - result = self.repo.run(['changes', '-s', 'submitted', '-m', PAGE_SIZE, self.repo_path]) + result = self.repo.run(['changes', '-s', 'submitted', path_with_revspec]) + result = [P4Changeset(cs) for cs in result] - # 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 1k results, than one with millions of results + return result def tag(self, name, user, revision=None, message=None, date=None, **opts): """ 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 100644 index 0000000..842ece0 --- /dev/null +++ b/vcs/tests/test_p4.py @@ -0,0 +1,38 @@ +import unittest +import logging + +import datetime + +from vcs.backends.p4.repository import P4Repository + +TEST_P4_REPO = '//depot/Tools/p4sandbox/...' + + +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(self): + CLs = self.repo.get_changesets() + + for cl in CLs: + logging.debug('%s, %s', cl, cl.time) + + self.assertEqual(len(CLs), 5) + + def test_get_changelists_range(self): + CLs = self.repo.get_changesets(start_date=datetime.datetime(2013,5,3,3)) + + for cl in CLs: + logging.debug('%s, %s', cl, cl.time) + + self.assertEqual(len(CLs), 2) + + +if __name__ == '__main__': + unittest.main() From 21c54552892c483c75b42a752e13e770bc5e179f Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Thu, 7 Jan 2016 04:35:33 -0800 Subject: [PATCH 04/14] improved the tests so unexpected commits to that path don't break them --- vcs/tests/test_p4.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vcs/tests/test_p4.py b/vcs/tests/test_p4.py index 842ece0..0e9453d 100644 --- a/vcs/tests/test_p4.py +++ b/vcs/tests/test_p4.py @@ -13,12 +13,13 @@ 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(self): - CLs = self.repo.get_changesets() + 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.time) @@ -26,7 +27,8 @@ def test_get_changelists(self): self.assertEqual(len(CLs), 5) def test_get_changelists_range(self): - CLs = self.repo.get_changesets(start_date=datetime.datetime(2013,5,3,3)) + 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.time) From 4b46b3f39d2838fa6b3e1dcc446ecd3ca80f74ad Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Thu, 7 Jan 2016 05:05:55 -0800 Subject: [PATCH 05/14] docstrings --- vcs/backends/p4/changeset.py | 42 +++++++++++++++++++++++------------ vcs/backends/p4/repository.py | 10 +++++---- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/vcs/backends/p4/changeset.py b/vcs/backends/p4/changeset.py index f57566a..158d02e 100755 --- a/vcs/backends/p4/changeset.py +++ b/vcs/backends/p4/changeset.py @@ -13,49 +13,63 @@ class P4Changeset(BaseChangeset): repository object within which changeset exists ``id`` - may be ``raw_id`` or i.e. for mercurial's tip just ``tip`` + Changelist number (int) ``raw_id`` - raw changeset representation (i.e. full 40 length sha for git - backend) + same as id ``short_id`` - shortened (if apply) version of ``raw_id``; it would be simple - shortcut for ``raw_id[:12]`` for git/mercurial backends or same - as ``raw_id`` for subversion + same as id ``revision`` - revision number as integer + same as id ``files`` - list of ``FileNode`` (``Node`` with NodeKind.FILE) objects + list of ``FileNode`` (``Node`` with NodeKind.FILE) objects, TBD ``dirs`` - list of ``DirNode`` (``Node`` with NodeKind.DIR) objects + list of ``DirNode`` (``Node`` with NodeKind.DIR) objects, TBD ``nodes`` - combined list of ``Node`` objects + combined list of ``Node`` objects, TBD ``author`` - author of the changeset, as unicode + author of the changeset, as unicode, TBD ``message`` message of the changeset, as unicode ``parents`` - list of parent changesets + 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`` + changesets would raise ``EmptyRepositoryError``, TBD + + Added properties: + + ``time`` + datetime object representing date and time of the submit + + ``raw_data`` + the raw dict returned by p4 lib or cmd """ def __init__(self, changeset_dict): + """ + + :param changeset_dict: the raw dict returned by p4 cmd or lib + :return: + """ self.revision = int(changeset_dict['change']) self.short_id = self.revision self.id = self.revision - self.time = datetime.datetime.utcfromtimestamp(int(changeset_dict['time'])) + + self.author = changeset_dict['user'] + self.message = changeset_dict['desc'] + self.raw_data = changeset_dict + self.time = datetime.datetime.utcfromtimestamp(int(changeset_dict['time'])) @LazyProperty def parents(self): diff --git a/vcs/backends/p4/repository.py b/vcs/backends/p4/repository.py index ef0b2ab..71af3f4 100755 --- a/vcs/backends/p4/repository.py +++ b/vcs/backends/p4/repository.py @@ -41,10 +41,12 @@ def __init__(self, repo_path, create=False, user=None, passwd=None, port=None, p :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: if set, used for authorization. If none, taken from env var - :param p4passwd=None: same as p4user - :param p4port=None same as p4user - :param p4client=None same as p4port + :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 From 77e1f215d4a094dc952fb6f2afbf2a19e77dce9b Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Thu, 7 Jan 2016 08:38:58 -0800 Subject: [PATCH 06/14] added p4 to backends --- vcs/conf/settings.py | 1 + 1 file changed, 1 insertion(+) 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 = { From 9344b381377f9c6738c628b2890d8254d0a5353d Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Thu, 7 Jan 2016 09:24:38 -0800 Subject: [PATCH 07/14] p4: changeset: time property renamed to date --- vcs/backends/p4/changeset.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vcs/backends/p4/changeset.py b/vcs/backends/p4/changeset.py index 158d02e..0b13801 100755 --- a/vcs/backends/p4/changeset.py +++ b/vcs/backends/p4/changeset.py @@ -47,11 +47,11 @@ class P4Changeset(BaseChangeset): otherwise; trying to access this attribute while there is no changesets would raise ``EmptyRepositoryError``, TBD - Added properties: - - ``time`` + ``date`` datetime object representing date and time of the submit + Added properties: + ``raw_data`` the raw dict returned by p4 lib or cmd """ @@ -69,7 +69,7 @@ def __init__(self, changeset_dict): self.message = changeset_dict['desc'] self.raw_data = changeset_dict - self.time = datetime.datetime.utcfromtimestamp(int(changeset_dict['time'])) + self.date = datetime.datetime.utcfromtimestamp(int(changeset_dict['time'])) @LazyProperty def parents(self): From c9565330c47530ee42d821fade62be03b28f34e9 Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Thu, 7 Jan 2016 10:29:05 -0800 Subject: [PATCH 08/14] p4: changeset: time property renamed to date, look for p4 also in /opt/perforce/bin, where it is installed by yum --- vcs/backends/p4/common.py | 8 +++++++- vcs/tests/test_p4.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/vcs/backends/p4/common.py b/vcs/backends/p4/common.py index 6e05267..956829d 100755 --- a/vcs/backends/p4/common.py +++ b/vcs/backends/p4/common.py @@ -1,5 +1,7 @@ import abc +import distutils.spawn import logging +import os import subprocess import marshal @@ -41,11 +43,15 @@ class SubprocessP4(P4): ARRAY_KEY = re.compile(r'(\w+?)(\d+)$') INT = re.compile(r'\d+$') + def __init__(self, user, passwd, port, client): + super(SubprocessP4, self).__init__(user, passwd, port, client) + self.p4bin = distutils.spawn.find_executable('p4', path='/opt/perforce/bin:%s' % os.environ['PATH']) + 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, + p = subprocess.Popen([self.p4bin, '-G'] + map(str, args), stdin=stdin_mode, stdout=subprocess.PIPE, env=env or self.env) if input is not None: diff --git a/vcs/tests/test_p4.py b/vcs/tests/test_p4.py index 0e9453d..a0e5aeb 100644 --- a/vcs/tests/test_p4.py +++ b/vcs/tests/test_p4.py @@ -22,7 +22,7 @@ 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.time) + logging.debug('%s, %s', cl, cl.date) self.assertEqual(len(CLs), 5) @@ -31,7 +31,7 @@ def test_get_changelists_range(self): end_date=datetime.datetime(2014, 1, 1)) for cl in CLs: - logging.debug('%s, %s', cl, cl.time) + logging.debug('%s, %s', cl, cl.date) self.assertEqual(len(CLs), 2) From be93b592e93a4d29f6d31b10eaa22ee0ac372218 Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Mon, 11 Jan 2016 04:08:46 -0800 Subject: [PATCH 09/14] reverted looking for p4 in different directories - it works out of box with helix-cli rpm. It creates the symlink in post-install script --- vcs/backends/p4/common.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/vcs/backends/p4/common.py b/vcs/backends/p4/common.py index 956829d..6e05267 100755 --- a/vcs/backends/p4/common.py +++ b/vcs/backends/p4/common.py @@ -1,7 +1,5 @@ import abc -import distutils.spawn import logging -import os import subprocess import marshal @@ -43,15 +41,11 @@ class SubprocessP4(P4): ARRAY_KEY = re.compile(r'(\w+?)(\d+)$') INT = re.compile(r'\d+$') - def __init__(self, user, passwd, port, client): - super(SubprocessP4, self).__init__(user, passwd, port, client) - self.p4bin = distutils.spawn.find_executable('p4', path='/opt/perforce/bin:%s' % os.environ['PATH']) - 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([self.p4bin, '-G'] + map(str, args), stdin=stdin_mode, stdout=subprocess.PIPE, + p = subprocess.Popen(['p4', '-G'] + map(str, args), stdin=stdin_mode, stdout=subprocess.PIPE, env=env or self.env) if input is not None: From fe0b3bc674669331383b6819cf96fb993918403d Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Tue, 12 Jan 2016 05:36:08 -0800 Subject: [PATCH 10/14] p4: full length of commit message --- vcs/backends/p4/repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcs/backends/p4/repository.py b/vcs/backends/p4/repository.py index 71af3f4..f446619 100755 --- a/vcs/backends/p4/repository.py +++ b/vcs/backends/p4/repository.py @@ -113,7 +113,7 @@ def get_changesets(self, start=None, end=None, start_date=None, start_date=start_date.strftime(STR_FORMAT), end_date=end_date.strftime(STR_FORMAT)) - result = self.repo.run(['changes', '-s', 'submitted', path_with_revspec]) + result = self.repo.run(['changes', '-l', '-s', 'submitted', path_with_revspec]) result = [P4Changeset(cs) for cs in result] return result From 26ee330cf1d66be573c1b3f70be8222dcc13564d Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Wed, 13 Jan 2016 10:12:01 -0800 Subject: [PATCH 11/14] p4: repo: added get_changeset, get_changests now supports IDs, changeset: added affected_files --- vcs/backends/p4/changeset.py | 16 +++++++++++-- vcs/backends/p4/common.py | 3 +++ vcs/backends/p4/repository.py | 45 ++++++++++++++++++++++++++--------- vcs/tests/test_p4.py | 6 +++++ 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/vcs/backends/p4/changeset.py b/vcs/backends/p4/changeset.py index 0b13801..18502fe 100755 --- a/vcs/backends/p4/changeset.py +++ b/vcs/backends/p4/changeset.py @@ -55,12 +55,13 @@ class P4Changeset(BaseChangeset): ``raw_data`` the raw dict returned by p4 lib or cmd """ - def __init__(self, changeset_dict): + 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 @@ -70,6 +71,7 @@ def __init__(self, changeset_dict): self.raw_data = changeset_dict self.date = datetime.datetime.utcfromtimestamp(int(changeset_dict['time'])) + self._describe_result = None @LazyProperty def parents(self): @@ -251,3 +253,13 @@ 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]) + + return self._describe_result + + def affected_files(self): + return self._describe()[0]['depotFile'] + diff --git a/vcs/backends/p4/common.py b/vcs/backends/p4/common.py index 6e05267..1321777 100755 --- a/vcs/backends/p4/common.py +++ b/vcs/backends/p4/common.py @@ -147,3 +147,6 @@ def decode_arrays(h): 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 index f446619..f3b74f7 100755 --- a/vcs/backends/p4/repository.py +++ b/vcs/backends/p4/repository.py @@ -3,7 +3,7 @@ import datetime from vcs.backends.base import BaseRepository -from .common import SubprocessP4 +from .common import get_p4_class from .changeset import P4Changeset import vcs.exceptions from vcs.utils.lazy import LazyProperty @@ -60,7 +60,8 @@ def __init__(self, repo_path, create=False, user=None, passwd=None, port=None, p client = p4client or os.environ.get('P4CLIENT') # this one isn't mandatory for read operations self.repo_path = repo_path - self.repo = SubprocessP4(user, passwd, port, client) + p4class = get_p4_class() + self.repo = p4class(user, passwd, port, client) def is_valid(self): """ @@ -79,7 +80,15 @@ def get_changeset(self, revision=None): :raises ``EmptyRepositoryError``: if there are no revisions """ - raise NotImplementedError + 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): @@ -103,18 +112,32 @@ def get_changesets(self, start=None, end=None, start_date=None, STR_FORMAT = '%Y/%m/%d %H:%M:%S' - if not start_date: - start_date = datetime.datetime.utcfromtimestamp(0) # january 1970 + 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 not end_date: - end_date = datetime.datetime.utcnow() + 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_date},{end_date}'.format(path=self.repo_path, - start_date=start_date.strftime(STR_FORMAT), - end_date=end_date.strftime(STR_FORMAT)) + 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(cs) for cs in result] + result = [P4Changeset(self, cs) for cs in result] return result diff --git a/vcs/tests/test_p4.py b/vcs/tests/test_p4.py index a0e5aeb..9a50388 100644 --- a/vcs/tests/test_p4.py +++ b/vcs/tests/test_p4.py @@ -35,6 +35,12 @@ def test_get_changelists_range(self): 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') + if __name__ == '__main__': unittest.main() From 535c5b67be60d0f52a8b4b36be5a4a90c9fb44a6 Mon Sep 17 00:00:00 2001 From: Michel Samia Date: Wed, 13 Jan 2016 13:08:13 -0800 Subject: [PATCH 12/14] get ready to errors in listing affected files --- vcs/backends/p4/changeset.py | 10 +++++++++- vcs/tests/test_p4.py | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/vcs/backends/p4/changeset.py b/vcs/backends/p4/changeset.py index 18502fe..bd7823e 100755 --- a/vcs/backends/p4/changeset.py +++ b/vcs/backends/p4/changeset.py @@ -1,4 +1,5 @@ import datetime +import logging from vcs.backends.base import BaseChangeset from vcs.utils.lazy import LazyProperty @@ -258,8 +259,15 @@ 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): - return self._describe()[0]['depotFile'] + try: + return self._describe()[-1]['depotFile'] + except: + logging.warning('No files in that changest %d', self.id) + return [] diff --git a/vcs/tests/test_p4.py b/vcs/tests/test_p4.py index 9a50388..ec94e25 100644 --- a/vcs/tests/test_p4.py +++ b/vcs/tests/test_p4.py @@ -41,6 +41,11 @@ def test_get_affected_files(self): 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() From a1e4b43913469f5178861cd5deefbcd5409bf437 Mon Sep 17 00:00:00 2001 From: msamia Date: Fri, 11 Mar 2016 17:11:47 +0100 Subject: [PATCH 13/14] add tz, add vagrantfile --- Vagrantfile | 79 ++++++++++++++++++++++++++++++++++++ setup.py | 2 +- vcs/backends/p4/changeset.py | 7 +++- vcs/backends/p4/common.py | 2 +- 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100755 Vagrantfile mode change 100644 => 100755 setup.py 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/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/changeset.py b/vcs/backends/p4/changeset.py index bd7823e..e700850 100755 --- a/vcs/backends/p4/changeset.py +++ b/vcs/backends/p4/changeset.py @@ -1,6 +1,8 @@ import datetime import logging +import pytz + from vcs.backends.base import BaseChangeset from vcs.utils.lazy import LazyProperty @@ -49,7 +51,7 @@ class P4Changeset(BaseChangeset): changesets would raise ``EmptyRepositoryError``, TBD ``date`` - datetime object representing date and time of the submit + datetime object representing date and time of the submit. TZ aware. Added properties: @@ -71,7 +73,8 @@ def __init__(self, repository, changeset_dict): self.message = changeset_dict['desc'] self.raw_data = changeset_dict - self.date = datetime.datetime.utcfromtimestamp(int(changeset_dict['time'])) + 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 diff --git a/vcs/backends/p4/common.py b/vcs/backends/p4/common.py index 1321777..3e49a32 100755 --- a/vcs/backends/p4/common.py +++ b/vcs/backends/p4/common.py @@ -24,7 +24,7 @@ def __init__(self, user, passwd, port, client, env=None): def run(self, args, input=None): """ - :param args: + :param args: list of arguments, e.g. ['foo', 'bar'] :param input: :return: """ From 7f68e3dd99e55c06db921478ddf5c27f919905ad Mon Sep 17 00:00:00 2001 From: msamia Date: Fri, 11 Mar 2016 17:20:29 +0100 Subject: [PATCH 14/14] public p4 server... --- vcs/tests/test_p4.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) mode change 100644 => 100755 vcs/tests/test_p4.py diff --git a/vcs/tests/test_p4.py b/vcs/tests/test_p4.py old mode 100644 new mode 100755 index ec94e25..1e22360 --- a/vcs/tests/test_p4.py +++ b/vcs/tests/test_p4.py @@ -5,7 +5,13 @@ from vcs.backends.p4.repository import P4Repository -TEST_P4_REPO = '//depot/Tools/p4sandbox/...' +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):