source: tailor/vcpx/repository/cg.py @ 1216

Revision 1216, 6.2 KB checked in by Adeodato Simo <dato@…>, 7 years ago (diff)

[general] Ensure Changeset.date always has timezone information

This patch modifies all source backends to always set the tzinfo member of
every Changeset.date they create, and all target backends to make proper use
of it at commit time. This should solve all offset errors in dates for
commits, and making tailor robust with respect implementing a "timezone"
option for a project.

Summary of the needed changes:

+ vcpx:

  • tzinfo.py: new file, taken from pytz sources. Provides definitions for two basic tzinfo classes: UTC, and FixedOffset?.
  • changes.py: make "date" member a property, with a setter function that raises an exception if the provided date does not have a not-None tzinfo member. Prefer this to silently setting it to UTC, which may be a wrong assumption.

+ vcpx/repository:

  • [source] cvs.py, cvsps.py darcs.py, monotone.py, svn.py: Changeset.date was always created in UTC; explicitly set date.tzinfo to UTC from tzinfo.py.
  • [source] bzr.py, git.py, hg.py: an UTC date was created from timestamp and offset; instead, create a date in the proper FixedOffset? timezone.
  • [source] tla.py: an UTC date was created from the Standard-date header; however, a Date header with the local date is also provided: add new function parse_date() that can calculate the timezone from these two headers, and use it.
  • [target] cvs.py, cvsps.py: str(date) was assumed not to contain timezone information; make date a tzinfo-less datetime prior to str()'ing it.
  • [target] cdv.py, darcs.py, monotone.py, svn.py: Changeset.date was assumed to be UTC; make the appropriate conversion prior to using it.
  • [target] cg.py, git.py: include "%z" in the strftime format specifier for GIT_AUTHOR_DATE.
  • [target] bzr.py, hg.py: do not use time.mktime() to calculate the timestamp, since it takes into account local timezone; use calendar.timegm() instead, which gives an UTC timestamp. And provide an appropriate timezone to the underlying commit() function.

+ vcpx/tests:

  • cvs.py, cvsps.py, darcs.py, svn.py: set tzinfo=UTC in datetimes that get created to be compared to cset.date.
Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Git target (using cogito)
3# :Creato:   Wed 24 ago 2005 18:34:27 EDT
4# :Autore:   Todd Mokros <tmokros@tmokros.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9This module implements the backend for Git by using Cogito.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from vcpx.repository import Repository
15from vcpx.shwrap import ExternalCommand
16from vcpx.target import SynchronizableTargetWorkingDir, TargetInitializationFailure
17from vcpx.source import ChangesetApplicationFailure
18
19
20class CgRepository(Repository):
21    METADIR = '.git'
22
23    def _load(self, project):
24        Repository._load(self, project)
25        self.EXECUTABLE = project.config.get(self.name, 'cg-command', 'cg')
26
27    def create(self):
28        """
29        Execute ``cg init``.
30        """
31
32        from os.path import join, exists
33
34        if exists(join(self.basedir, self.METADIR)):
35            return
36
37        cmd = self.command("init", "-I")
38        init = ExternalCommand(cwd=self.basedir, command=cmd)
39        init.execute()
40
41        if init.exit_status:
42            raise TargetInitializationFailure(
43                "%s returned status %s" % (str(init), init.exit_status))
44
45
46class CgWorkingDir(SynchronizableTargetWorkingDir):
47
48    ## SynchronizableTargetWorkingDir
49
50    def _addPathnames(self, names):
51        """
52        Add some new filesystem objects.
53        """
54
55        from os.path import join, isdir
56
57        # Currently git/cogito does not handle directories at all, so filter
58        # them out.
59
60        notdirs = [n for n in names if not isdir(join(self.repository.basedir, n))]
61        if notdirs:
62            cmd = self.repository.command("add")
63            ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(notdirs)
64
65    def __parse_author(self, author):
66        """
67        Parse the author field, returning (name, email)
68        """
69        from email.Utils import parseaddr
70        from vcpx.target import AUTHOR, HOST
71
72        if author.find('@') > -1:
73            name, email = parseaddr(author)
74        else:
75            name, email = author, ''
76        name = name.strip()
77        email = email.strip()
78        if not name:
79            name = AUTHOR
80        if not email:
81            email = "%s@%s" % (AUTHOR, HOST)
82        return (name, email)
83
84    def _commit(self, date, author, patchname, changelog=None, entries=None):
85        """
86        Commit the changeset.
87        """
88
89        from os import environ
90
91        encode = self.repository.encode
92
93        logmessage = []
94        if patchname:
95            logmessage.append(patchname)
96        if changelog:
97            logmessage.append(changelog)
98
99        env = {}
100        env.update(environ)
101
102        (name, email) = self.__parse_author(author)
103        if name:
104            env['GIT_AUTHOR_NAME'] = encode(name)
105        if email:
106            env['GIT_AUTHOR_EMAIL']=email
107        if date:
108            env['GIT_AUTHOR_DATE']=date.strftime('%Y-%m-%d %H:%M:%S %z')
109        # '-f' flag means we can get empty commits, which
110        # shouldn't be a problem.
111        cmd = self.repository.command("commit", "-f")
112        c = ExternalCommand(cwd=self.repository.basedir, command=cmd)
113
114        c.execute(env=env, input=encode('\n'.join(logmessage)))
115        if c.exit_status:
116            raise ChangesetApplicationFailure("%s returned status %d" %
117                                              (str(c), c.exit_status))
118
119    def _removePathnames(self, names):
120        """
121        Remove some filesystem object.
122        """
123
124        from os.path import join, isdir
125        # Currently git does not handle directories at all, so filter
126        # them out.
127
128        notdirs = [n for n in names if not isdir(join(self.repository.basedir, n))]
129        if notdirs:
130            cmd = self.repository.command("rm")
131            c=ExternalCommand(cwd=self.repository.basedir, command=cmd)
132            c.execute(notdirs)
133
134    def _renamePathname(self, oldname, newname):
135        """
136        Rename a filesystem object.
137        """
138        # In the future, we may want to switch to using
139        # git rename, in case renames ever get more support
140        # in git.  It currently just does and add and remove.
141        from os.path import join, isdir
142        from os import walk
143        from vcpx.dualwd import IGNORED_METADIRS
144
145        if isdir(join(self.repository.basedir, newname)):
146            # Given lack of support for directories in current Git,
147            # loop over all files under the new directory and
148            # do a add/remove on them.
149            skip = len(self.repository.basedir)+len(newname)+2
150            for dir, subdirs, files in walk(join(self.repository.basedir, newname)):
151                prefix = dir[skip:]
152
153                for excd in IGNORED_METADIRS:
154                    if excd in subdirs:
155                        subdirs.remove(excd)
156
157                for f in files:
158                    self._removePathnames([join(oldname, prefix, f)])
159                    self._addPathnames([join(newname, prefix, f)])
160        else:
161            self._removePathnames([oldname])
162            self._addPathnames([newname])
163
164    def _prepareTargetRepository(self):
165        self.repository.create()
166
167    def _prepareWorkingDirectory(self, source_repo):
168        """
169        Create the .git/info/exclude.
170        """
171
172        from os.path import join
173        from vcpx.dualwd import IGNORED_METADIRS
174
175        # Create the .git/info/exclude file, that contains an
176        # fnmatch per line with metadirs to be skipped.
177        ignore = open(join(self.repository.basedir, self.repository.METADIR,
178                           'info', 'exclude'), 'a')
179        ignore.write('\n')
180        ignore.write('\n'.join(['%s' % md
181                                for md in IGNORED_METADIRS]))
182        ignore.write('\n')
183        if self.logfile.startswith(self.repository.basedir):
184            ignore.write(self.logfile[len(self.repository.basedir)+1:])
185            ignore.write('\n')
186        if self.state_file.filename.startswith(self.repository.basedir):
187            sfrelname = self.state_file.filename[len(self.repository.basedir)+1:]
188            ignore.write(sfrelname)
189            ignore.write('\n')
190            ignore.write(sfrelname+'.old')
191            ignore.write('\n')
192            ignore.write(sfrelname+'.journal')
193            ignore.write('\n')
194        ignore.close()
Note: See TracBrowser for help on using the repository browser.