source: tailor/vcpx/repository/git/target.py @ 1652

Revision 1652, 11.4 KB checked in by jhs@…, 5 years ago (diff)

Support git target renaming of empty directories in disjunct operation

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Git target (using git-core)
3# :Creato:   Thu  1 Sep 2005 04:01:37 EDT
4# :Autore:   Todd Mokros <tmokros@tmokros.net>
5#            Brendan Cully <brendan@kublai.com>
6#            Yann Dirson <ydirson@altern.org>
7# :Licenza:  GNU General Public License
8#
9
10"""
11This module implements the target backend for Git using git-core.
12"""
13
14__docformat__ = 'reStructuredText'
15
16from vcpx import TailorException
17from vcpx.config import ConfigurationError
18from vcpx.repository.git import GitExternalCommand, PIPE
19from vcpx.source import ChangesetApplicationFailure
20from vcpx.target import SynchronizableTargetWorkingDir, TargetInitializationFailure
21from vcpx.tzinfo import FixedOffset
22
23
24class BranchpointFailure(TailorException):
25    "Specified branchpoint not found in parent branch"
26
27
28class GitTargetWorkingDir(SynchronizableTargetWorkingDir):
29
30    def _addPathnames(self, names):
31        """
32        Add some new filesystem objects.
33        """
34
35        from os.path import join, isdir
36
37        # Currently git does not handle directories at all, so filter
38        # them out.
39
40        notdirs = [n for n in names if not isdir(join(self.repository.basedir, n))]
41        if notdirs:
42            self.repository.runCommand(['update-index', '--add'] + notdirs)
43
44    def _editPathnames(self, names):
45        """
46        Records a sequence of filesystem objects as updated.
47        """
48
49        from os.path import join, isdir
50
51        # can we assume we don't have directories in the list ?  Nope.
52
53        notdirs = [n for n in names if not isdir(join(self.repository.basedir, n))]
54        if notdirs:
55            self.repository.runCommand(['update-index'] + notdirs)
56
57    def __parse_author(self, author):
58        """
59        Parse the author field, returning (name, email)
60        """
61
62        from email.Utils import parseaddr
63        from vcpx.target import AUTHOR, HOST
64
65        if author.find('@') > -1:
66            name, email = parseaddr(author)
67        else:
68            name, email = author, ''
69        name = name.strip()
70        email = email.strip()
71        if not name:
72            name = AUTHOR
73        if not email:
74            email = "%s@%s" % (name, HOST)
75        return (name, email)
76
77    def _commit(self, date, author, patchname, changelog=None, entries=None,
78                tags=[], isinitialcommit=False):
79        """
80        Commit the changeset.
81        """
82
83        from os import environ
84
85        try:
86            self.repository.runCommand(['status'])
87        except Exception, e:
88            self.log.info("git-status returned an error---assuming nothing to do")
89            return
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        # update the index
103        self.repository.runCommand(['add', '-u'])
104        treeid = self.repository.runCommand(['write-tree'])[0]
105
106        # in single-repository mode, only update the relevant branch
107        if self.repository.branch_name:
108            refname = self.repository.branch_name
109        else:
110            refname = 'HEAD'
111
112        # find the previous commit on the branch if any
113        c = GitExternalCommand(self.repository, cwd=self.repository.basedir,
114                               command=self.repository.command('rev-parse', refname))
115        (out, err) = c.execute(stdout=PIPE, stderr=PIPE)
116        if c.exit_status:
117            # Do we need to check err to be sure there was no error ?
118            self.log.info("Doing initial commit")
119            parent = False
120        else:
121            # FIXME: I'd prefer to avoid all those "if parent"
122            parent = out.read().split('\n')[0]
123
124        (name, email) = self.__parse_author(author)
125        if name:
126            env['GIT_AUTHOR_NAME'] = encode(name)
127            env['GIT_COMMITTER_NAME'] = encode(name)
128        if email:
129            env['GIT_AUTHOR_EMAIL']=email
130            env['GIT_COMMITTER_EMAIL']=email
131        if date:
132            env['GIT_AUTHOR_DATE']=date.strftime("%Y-%m-%d %H:%M:%S %z")
133            env['GIT_COMMITTER_DATE']=env['GIT_AUTHOR_DATE']
134        if parent:
135            cmd = self.repository.command('commit-tree', treeid, '-p', parent)
136        else:
137            cmd = self.repository.command('commit-tree', treeid)
138        c = GitExternalCommand(self.repository, cwd=self.repository.basedir, command=cmd)
139
140        logmessage = encode('\n'.join(logmessage))
141        if not logmessage:
142            logmessage = 'No commit message\n'
143        if not logmessage.endswith('\n'):
144            logmessage += '\n'
145        (out, _) = c.execute(stdout=PIPE, env=env, input=logmessage)
146        if c.exit_status:
147            failed = True
148            if out:
149                for line in [x.strip() for x in out if x[0] != '#']:
150                    if line == 'nothing to commit':
151                        failed = False
152            if failed:
153                raise ChangesetApplicationFailure("%s returned status %d" %
154                                                  (str(c), c.exit_status))
155        else:
156            commitid=out.read().split('\n')[0]
157
158            if parent:
159                self.repository.runCommand(['update-ref', refname, commitid, parent])
160            else:
161                self.repository.runCommand(['update-ref', refname, commitid])
162
163    def _tag(self, tag, date, author):
164
165        # in single-repository mode, only update the relevant branch
166        if self.repository.branch_name:
167            refname = self.repository.branch_name
168        else:
169            refname = 'HEAD'
170
171        # Allow a new tag to overwrite an older one with -f
172        args = ["tag", "-a",]
173        if self.repository.overwrite_tags:
174            args.append("-f")
175
176        # Escape the tag name for git
177        import re
178        tag_git = re.sub('_*$', '', re.sub('__', '_', re.sub('[^A-Za-z0-9_-]', '_', tag)))
179
180        args += ["-m", tag, tag_git, refname]
181        cmd = self.repository.command(*args)
182        c = GitExternalCommand(self.repository, cwd=self.repository.basedir, command=cmd)
183        from os import environ
184        env = {}
185        env.update(environ)
186        (name, email) = self.__parse_author(author)
187        if name:
188            env['GIT_AUTHOR_NAME'] = self.repository.encode(name)
189            env['GIT_COMMITTER_NAME'] = self.repository.encode(name)
190        if email:
191            env['GIT_AUTHOR_EMAIL']=email
192            env['GIT_COMMITTER_EMAIL']=email
193        if date:
194            env['GIT_AUTHOR_DATE']=date.strftime("%Y-%m-%d %H:%M:%S %z")
195            env['GIT_COMMITTER_DATE']=env['GIT_AUTHOR_DATE']
196        c.execute(env=env)
197
198        if c.exit_status:
199            if not self.repository.overwrite_tags:
200                self.log.critical("Couldn't set tag '%s': maybe it's a "
201                                  "conflict with a previous tag, and "
202                                  "overwrite-tags=True may help" %
203                                  tag_git)
204            raise ChangesetApplicationFailure("%s returned status %d" %
205                                              (str(c), c.exit_status))
206
207    def _removePathnames(self, names):
208        """
209        Remove some filesystem object.
210        """
211
212        from os.path import join, isdir
213
214        # Currently git does not handle directories at all, so filter
215        # them out.
216
217        notdirs = [n for n in names if not isdir(join(self.repository.basedir, n))]
218        if notdirs:
219            self.repository.runCommand(['update-index', '--remove'] + notdirs)
220
221    def _renamePathname(self, oldname, newname):
222        """
223        Rename a filesystem object.
224        """
225
226        # Git does not seem to allow
227        #   $ mv a.txt b.txt
228        #   $ git mv a.txt b.txt
229        # Here we are in this situation, since upstream VCS already
230        # moved the item.
231
232        from os import mkdir, rename, rmdir, listdir
233        from os.path import join, exists, isdir
234
235        oldpath = join(self.repository.basedir, oldname)
236        newpath = join(self.repository.basedir, newname)
237
238        # These are used with disjunct directories.
239        newpathtmp = newpath + '-TAILOR-HACKED-TEMP-NAME'
240        newnametmp = newname + '-TAILOR-HACKED-TEMP-NAME'
241
242        # Git does not track empty directories, so if there is only an
243        # empty dir, we have nothing to do.
244        if (isdir(newpath) and not len(listdir(newpath))) or \
245           (isdir(newpathtmp) and not len(listdir(newpathtmp))):
246            return
247
248        # rename() won't work for rename(a/b, a)
249        if newpath.startswith(oldpath+"/"):
250            oldpathtmp = oldpath+"-TAILOR-HACKED-TEMP-NAME"
251            oldnametmp = oldname+"-TAILOR-HACKED-TEMP-NAME"
252            if exists(oldpathtmp):
253                rename(oldpathtmp, oldpath)
254            rename(newpath, oldpathtmp)
255            rmdir(oldpath)
256            rename(oldpathtmp, oldpath)
257            mkdir(oldpathtmp)
258            self.repository.runCommand(['mv', oldname, newname.replace(oldname, oldnametmp, 1)])
259            self.repository.runCommand(['mv', oldnametmp, oldname])
260        else:
261            if self.shared_basedirs:
262                # we can just add the new path, commit will detect the
263                # deleted ones automatically
264                self.repository.runCommand(['add', newname])
265            else:
266                # For disjunct directories, the real new entry has been moved
267                # out of the way, and the superclass expects us to rename the
268                # the file or directory via git.
269                if exists(newpath) or not exists(newpathtmp):
270                    raise ChangesetApplicationFailure("Unsure how to handle disjunct rename of %s"
271                                                          % newname)
272                self.repository.runCommand(['mv', oldname, newname])
273
274    def _prepareTargetRepository(self):
275        self.repository.create()
276
277    def _prepareWorkingDirectory(self, source_repo):
278        """
279        Create the .git/info/exclude.
280        """
281
282        from os.path import join, exists
283        from os import mkdir
284        from vcpx.dualwd import IGNORED_METADIRS
285
286        # create info/excludes in storagedir
287        infodir = join(self.repository.basedir, self.repository.storagedir, 'info')
288        if not exists(infodir):
289            mkdir(infodir)
290
291        # Create the .git/info/exclude file, that contains an
292        # fnmatch per line with metadirs to be skipped.
293        ignore = open(join(infodir, 'exclude'), 'a')
294        ignore.write('\n')
295        ignore.write('\n'.join(['%s' % md
296                                for md in IGNORED_METADIRS]))
297        ignore.write('\n')
298        if self.logfile.startswith(self.repository.basedir):
299            ignore.write(self.logfile[len(self.repository.basedir)+1:])
300            ignore.write('\n')
301        if self.state_file.filename.startswith(self.repository.basedir):
302            sfrelname = self.state_file.filename[len(self.repository.basedir)+1:]
303            ignore.write(sfrelname)
304            ignore.write('\n')
305            ignore.write(sfrelname+'.old')
306            ignore.write('\n')
307            ignore.write(sfrelname+'.journal')
308            ignore.write('\n')
309        ignore.close()
310
311    def importFirstRevision(self, source_repo, changeset, initial):
312        # If we have a parent repository, always track from INITIAL
313        SynchronizableTargetWorkingDir.importFirstRevision(
314            self, source_repo, changeset,
315            initial or self.repository.branch_point)
Note: See TracBrowser for help on using the repository browser.