source: tailor/vcpx/git.py @ 1163

Revision 1163, 12.3 KB checked in by lele@…, 7 years ago (diff)

M-x whitespace-cleanup

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# :Licenza:  GNU General Public License
7#
8
9"""
10This module implements the backend for Git using git-core.
11"""
12
13__docformat__ = 'reStructuredText'
14
15from shwrap import ExternalCommand, PIPE
16from source import UpdatableSourceWorkingDir, GetUpstreamChangesetsFailure
17from source import ChangesetApplicationFailure
18from target import SynchronizableTargetWorkingDir, TargetInitializationFailure
19
20class GitWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir):
21    ## UpdatableSourceWorkingDir
22    def _checkoutUpstreamRevision(self, revision):
23        """ git clone """
24        from os import rename, rmdir
25        from os.path import join
26
27        # Right now we clone the entire repository and just check out to the
28        # current rev because it makes revision parsing easier. We can't
29        # easily check out arbitrary revisions anyway, but we could probably
30        # handle HEAD (master) as a special case...
31        # git clone won't checkout into an existing directory
32        target = join(self.basedir, '.gittmp')
33        # might want -s if we can determine that the path is local. Then again,
34        # that makes it a little unsafe to do git write actions here
35        self._tryCommand(['clone', '-n', self.repository.repository, target],
36                         ChangesetApplicationFailure, False)
37
38        rename(join(target, '.git'), join(self.basedir, '.git'))
39        rmdir(target)
40
41        rev = self._getRev(revision)
42        if rev != revision:
43            self.log.info('Checking out revision %s (%s)' % (rev, revision))
44        else:
45            self.log.info('Checking out revision ' + rev)
46        self._tryCommand(['reset', '--hard', rev], ChangesetApplicationFailure, False)
47
48        return self._changesetForRevision(rev)
49
50    def _getUpstreamChangesets(self, since):
51        self._tryCommand(['fetch'], GetUpstreamChangesetsFailure, False)
52
53        revs = self._tryCommand(['rev-list', '^' + since, 'origin'],
54                                GetUpstreamChangesetsFailure)[:-1]
55        revs.reverse()
56        for rev in revs:
57            self.log.info('Updating to revision ' + rev)
58            yield self._changesetForRevision(rev)
59
60    def _applyChangeset(self, changeset):
61        out = self._tryCommand(['merge', '-n', '--no-commit', 'fastforward',
62                                'HEAD', changeset.revision],
63                         ChangesetApplicationFailure)
64
65        conflicts = []
66        for line in out:
67            if line.endswith(': needs update'):
68                conflicts.append(line[:-14])
69
70        if conflicts:
71            self.log.warning("Conflict after 'git merge': %s", ' '.join(conflicts))
72
73        return conflicts
74
75    def _changesetForRevision(self, revision):
76        from changes import Changeset, ChangesetEntry
77        from datetime import datetime
78
79        action_map = {'A': ChangesetEntry.ADDED, 'D': ChangesetEntry.DELETED,
80                      'M': ChangesetEntry.UPDATED, 'R': ChangesetEntry.RENAMED}
81
82        # find parent
83        lines = self._tryCommand(['rev-list', '--pretty=raw', '--max-count=1', revision],
84                                 GetUpstreamChangesetsFailure)
85        parents = []
86        user = Changeset.ANONYMOUS_USER
87        loglines = []
88        date = None
89        for line in lines:
90            if line.startswith('parent'):
91                parents.append(line.split(' ').pop())
92            if line.startswith('author'):
93                author_fields = line.split(' ')[1:]
94                tz = int(author_fields.pop())
95                dt = int(author_fields.pop())
96                user = ' '.join(author_fields)
97                tzsecs = abs(tz)
98                tzsecs = (tz / 100 * 60 + tz % 100) * 60
99                if tz < 0:
100                    tzsecs = -tzsecs
101                date = datetime.utcfromtimestamp(dt + tzsecs)
102            if line.startswith('    '):
103                loglines.append(line.lstrip('    '))
104
105        message = '\n'.join(loglines)
106        entries = []
107        cmd = ['diff-tree', '--root', '-r', '-M', '--name-status']
108        # haven't thought about merges yet...
109        if parents:
110            cmd.append(parents[0])
111        cmd.append(revision)
112        files = self._tryCommand(cmd, GetUpstreamChangesetsFailure)[:-1]
113        if not parents:
114            # git lets us know what it's diffing against if we omit parent
115            if len(files) > 0:
116                files.pop(0)
117        for line in files:
118            fields = line.split('\t')
119            state = fields.pop(0)
120            name = fields.pop()
121            e = ChangesetEntry(name)
122            e.action_kind = action_map[state[0]]
123            if e.action_kind == ChangesetEntry.RENAMED:
124                e.old_name = fields.pop()
125
126            entries.append(e)
127
128        # Brute-force tag search
129        from os.path import join
130        from os import listdir
131
132        tags = []
133        tagdir = join(self.basedir, '.git', 'refs', 'tags')
134        try:
135            for tag in listdir(tagdir):
136                # Consider caching stat info per tailor run
137                tagrev = self._tryCommand(['rev-list', '--max-count=1', tag])[0]
138                if (tagrev == revision):
139                    tags.append(tag)
140        except OSError:
141            # No tag dir
142            pass
143
144        return Changeset(revision, date, user, message, entries, tags=tags)
145
146    def _getRev(self, revision):
147        """ Return the git object corresponding to the symbolic revision """
148        if revision == 'INITIAL':
149            return self._tryCommand(['rev-list', 'HEAD'], GetUpstreamChangesetsFailure)[-2]
150
151        return self._tryCommand('rev-parse', '--verify', revision, GetUpstreamChangesetsFailure)[0]
152
153    def _tryCommand(self, cmd, exception=Exception, pipe=True):
154        c = ExternalCommand(command = self.repository.command(*cmd), cwd = self.basedir)
155        if pipe:
156            output = c.execute(stdout=PIPE)[0]
157        else:
158            c.execute()
159        if c.exit_status:
160            raise exception(str(c) + ' failed')
161        if pipe:
162            return output.read().split('\n')
163
164    ## SynchronizableTargetWorkingDir
165
166    def _addPathnames(self, names):
167        """
168        Add some new filesystem objects.
169        """
170
171        from os.path import join, isdir
172
173        # Currently git does not handle directories at all, so filter
174        # them out.
175
176        notdirs = [n for n in names if not isdir(join(self.basedir, n))]
177        if notdirs:
178            cmd = self.repository.command("add")
179            ExternalCommand(cwd=self.basedir, command=cmd).execute(notdirs)
180
181    def __parse_author(self, author):
182        """
183        Parse the author field, returning (name, email)
184        """
185        from email.Utils import parseaddr
186        from target import AUTHOR, HOST
187
188        if author.find('@') > -1:
189            name, email = parseaddr(author)
190        else:
191            name, email = author, ''
192        name = name.strip()
193        email = email.strip()
194        if not name:
195            name = AUTHOR
196        if not email:
197            email = "%s@%s" % (AUTHOR, HOST)
198        return (name, email)
199
200    def _commit(self, date, author, patchname, changelog=None, entries=None):
201        """
202        Commit the changeset.
203        """
204
205        from os import environ
206
207        encode = self.repository.encode
208
209        logmessage = []
210        if patchname:
211            logmessage.append(patchname)
212        if changelog:
213            logmessage.append(changelog)
214
215        env = {}
216        env.update(environ)
217
218        (name, email) = self.__parse_author(author)
219        if name:
220            env['GIT_AUTHOR_NAME'] = encode(name)
221            env['GIT_COMMITTER_NAME'] = encode(name)
222        if email:
223            env['GIT_AUTHOR_EMAIL']=email
224            env['GIT_COMMITTER_EMAIL']=email
225        if date:
226            env['GIT_AUTHOR_DATE']=date.strftime("%Y-%m-%d %H:%M:%S")
227            env['GIT_COMMITTER_DATE']=env['GIT_AUTHOR_DATE']
228        # '-f' flag means we can get empty commits, which
229        # shouldn't be a problem.
230        cmd = self.repository.command("commit", "-a", "-F", "-")
231        c = ExternalCommand(cwd=self.basedir, command=cmd)
232
233        logmessage = encode('\n'.join(logmessage))
234        if not logmessage:
235            logmessage = 'No commit message\n'
236        if not logmessage.endswith('\n'):
237            logmessage += '\n'
238        (out, _) = c.execute(stdout=PIPE, env=env, input=logmessage)
239        if c.exit_status:
240            failed = True
241            if out:
242                for line in [x.strip() for x in out if x[0] != '#']:
243                    if line == 'nothing to commit':
244                        failed = False
245            if failed:
246                raise ChangesetApplicationFailure("%s returned status %d" %
247                                                  (str(c), c.exit_status))
248
249    def _tag(self, tag):
250        # Allow a new tag to overwrite an older one with -f
251        cmd = self.repository.command("tag", "-f", tag)
252        c = ExternalCommand(cwd=self.basedir, command=cmd)
253        c.execute()
254
255        if c.exit_status:
256            raise ChangesetApplicationFailure("%s returned status %d" %
257                                              (str(c), c.exit_status))
258
259    def _removePathnames(self, names):
260        """
261        Remove some filesystem object.
262        git commit -a will automatically handle files deleted on the
263        filesystem, which should have already been done by the source
264        or rsync.
265        """
266        pass
267
268    def _renamePathname(self, oldname, newname):
269        """
270        Rename a filesystem object.
271        """
272        # In the future, we may want to switch to using
273        # git rename, in case renames ever get more support
274        # in git.  It currently just does and add and remove.
275        from os.path import join, isdir
276        from os import walk
277        from dualwd import IGNORED_METADIRS
278
279        if isdir(join(self.basedir, newname)):
280            # Given lack of support for directories in current Git,
281            # loop over all files under the new directory and
282            # do a add/remove on them.
283            skip = len(self.basedir)+len(newname)+2
284            for dir, subdirs, files in walk(join(self.basedir, newname)):
285                prefix = dir[skip:]
286
287                for excd in IGNORED_METADIRS:
288                    if excd in subdirs:
289                        subdirs.remove(excd)
290
291                for f in files:
292                    self._removePathnames([join(oldname, prefix, f)])
293                    self._addPathnames([join(newname, prefix, f)])
294        else:
295            self._removePathnames([oldname])
296            self._addPathnames([newname])
297
298    def _prepareTargetRepository(self):
299        """
300        Execute ``git init-db``.
301        """
302
303        from os.path import join, exists
304
305        if not exists(join(self.basedir, self.repository.METADIR)):
306            init = ExternalCommand(cwd=self.basedir,
307                                   command=self.repository.command("init-db"))
308            init.execute()
309
310            if init.exit_status:
311                raise TargetInitializationFailure(
312                    "%s returned status %s" % (str(init), init.exit_status))
313
314    def _prepareWorkingDirectory(self, source_repo):
315        """
316        Create the .git/info/exclude.
317        """
318
319        from os.path import join, exists
320        from os import mkdir
321        from dualwd import IGNORED_METADIRS
322
323        infodir = join(self.basedir, self.repository.METADIR, 'info')
324        if not exists(infodir):
325            mkdir(infodir)
326
327        # Create the .git/info/exclude file, that contains an
328        # fnmatch per line with metadirs to be skipped.
329        ignore = open(join(infodir, 'exclude'), 'a')
330        ignore.write('\n')
331        ignore.write('\n'.join(['%s' % md
332                                for md in IGNORED_METADIRS]))
333        ignore.write('\n')
334        if self.logfile.startswith(self.basedir):
335            ignore.write(self.logfile[len(self.basedir)+1:])
336            ignore.write('\n')
337        if self.state_file.filename.startswith(self.basedir):
338            sfrelname = self.state_file.filename[len(self.basedir)+1:]
339            ignore.write(sfrelname)
340            ignore.write('\n')
341            ignore.write(sfrelname+'.old')
342            ignore.write('\n')
343            ignore.write(sfrelname+'.journal')
344            ignore.write('\n')
345        ignore.close()
Note: See TracBrowser for help on using the repository browser.