source: tailor/vcpx/repository/monotone.py @ 1316

Revision 1316, 36.0 KB checked in by lele@…, 6 years ago (diff)

Fix exception message

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Monotone details
3# :Creato:   Tue Apr 12 01:28:10 CEST 2005
4# :Autore:   Markus Schiltknecht <markus@bluegap.ch>
5# :Autore:   Riccardo Ghetta <birrachiara@tin.it>
6# :Licenza:  GNU General Public License
7#
8
9"""
10This module contains supporting classes for Monotone.
11"""
12
13__docformat__ = 'reStructuredText'
14
15from os.path import exists, join, isdir
16from string import whitespace
17
18from vcpx.repository import Repository
19from vcpx.shwrap import ExternalCommand, PIPE, ReopenableNamedTemporaryFile
20from vcpx.source import UpdatableSourceWorkingDir, InvocationError, \
21                        ChangesetApplicationFailure, GetUpstreamChangesetsFailure
22from vcpx.target import SynchronizableTargetWorkingDir, TargetInitializationFailure
23from vcpx.changes import Changeset
24from vcpx.tzinfo import UTC
25
26
27MONOTONERC = """\
28function get_passphrase(KEYPAIR_ID)
29  return "%s"
30end
31"""
32
33class MonotoneRepository(Repository):
34    METADIR = '_MTN'
35
36    def _load(self, project):
37        Repository._load(self, project)
38        cget = project.config.get
39        self.EXECUTABLE = cget(self.name, 'monotone-command', 'mtn')
40        self.keyid = cget(self.name, 'keyid') or \
41                     cget(self.name, '%s-keyid' % self.which)
42        self.passphrase = cget(self.name, 'passphrase') or \
43                          cget(self.name, '%s-passphrase' % self.which)
44        self.keygenid = cget(self.name, 'keygenid') or \
45                        cget(self.name, '%s-keygenid' % self.which)
46        self.custom_lua = cget(self.name, 'custom_lua') or \
47                          cget(self.name, '%s-custom_lua' % self.which)
48
49    def create(self):
50        """
51        Create a new monotone DB, storing the commit keys, if available
52        """
53
54        if not self.repository or exists(self.repository):
55            return
56
57        cmd = self.command("db", "init", "--db", self.repository)
58        init = ExternalCommand(command=cmd)
59        init.execute(stdout=PIPE, stderr=PIPE)
60
61        if init.exit_status:
62            raise TargetInitializationFailure("Was not able to initialize "
63                                              "the monotone db at %r" %
64                                              self.repository)
65
66        if self.keyid:
67            self.log.info("Using key %s for commits" % (self.keyid,))
68        else:
69            # keystore key id unspecified, look at other options
70            if self.keygenid:
71                # requested a new key
72                cmd = self.command("genkey", "--db", self.repository)
73                regkey = ExternalCommand(command=cmd)
74                if self.passphrase:
75                    passp = "%s\n%s\n" % (self.passphrase, self.passphrase)
76                else:
77                    passp = None
78                regkey.execute(self.keygenid, input=passp, stdout=PIPE, stderr=PIPE)
79                if regkey.exit_status:
80                    raise TargetInitializationFailure("Was not able to setup "
81                                                  "the monotone initial key at %r" %
82                                                  self.repository)
83            else:
84                raise TargetInitializationFailure("Can't setup the monotone "
85                                                  "repository %r. "
86                                                  "A keyid or keygenid "
87                                                  "must be provided." %
88                                                  self.repository)
89
90
91
92class ExternalCommandChain:
93    """
94    This class implements command piping, i.e. a chain of
95    ExternalCommand, each feeding its stdout to the stdin of next
96    command in the chain. If a command fails, the chain breaks and
97    returns error.
98
99    Note:
100    This class implements only a subset of ExternalCommand functionality
101    """
102    def __init__(self, command, cwd=None):
103        self.commandchain =command
104        self.cwd = cwd
105        self.exit_status = 0
106
107    def execute(self):
108        outstr = None
109        for cmd in self.commandchain:
110            input = outstr
111            exc = ExternalCommand(cwd=self.cwd, command=cmd)
112            out, err = exc.execute(input=input, stdout=PIPE, stderr=PIPE)
113            self.exit_status = exc.exit_status
114            if self.exit_status:
115                break
116            outstr = out.getvalue()
117        return out, err
118
119
120class MonotoneChangeset(Changeset):
121    """
122    Monotone changesets differ from standard Changeset because:
123
124    1. only the "revision" field is used for eq/ne comparison
125    2. have additional properties used to handle history linearization
126    """
127
128    def __init__(self, linearized_ancestor, revision):
129        """
130        Initializes a new MonotoneChangeset. The linearized_ancestor
131        parameters is the fake ancestor used for linearization. The
132        very first revision tailorized has lin_ancestor==None
133        """
134
135        Changeset.__init__(self, revision=revision, date=None, author=None, log="")
136        self.lin_ancestor = linearized_ancestor
137        self.real_ancestors = None
138
139    def __eq__(self, other):
140        return (self.revision == other.revision)
141
142    def __ne__(self, other):
143        return (self.revision <> other.revision)
144
145    def __str__(self):
146        s = [Changeset.__str__(self)]
147        s.append('linearized ancestor: %s' % self.lin_ancestor)
148        s.append('real ancestor(s): %s' %
149                 (self.real_ancestors and ','.join(self.real_ancestors)
150                  or 'None'))
151        return '\n'.join(s)
152
153    def update(self, real_dates, authors, log, real_ancestors, branches):
154        """
155        Updates the monotone changeset secondary data
156        """
157        self.author=".".join(authors)
158        self.setLog(log)
159        self.date = real_dates[0]
160        self.real_dates = real_dates
161        self.real_ancestors = real_ancestors
162        self.branches = branches
163
164
165class MonotoneLogParser:
166    """
167    Obtain and parse a *single* "mtn log" output, reconstructing
168    the revision information
169    """
170
171    class PrefixRemover:
172        """
173        Helper class. Matches a prefix, allowing access to the text following
174        """
175        def __init__(self, str):
176            self.str = str
177            self.value=""
178
179        def __call__(self, prefix):
180            if self.str.startswith(prefix):
181                self.value = self.str[len(prefix):].strip()
182                return True
183            else:
184                return False
185
186    # logfile states
187    SINGLE = 0  # single line state
188    ADD = 1 # in add file/dir listing
189    MOD = 2 # in mod file/dir listing
190    DEL = 3 # in delete file/dir listing
191    REN = 4 # in renamed file/dir listing
192    LOG = 5 # in changelog listing
193    CMT = 6 # in comment listing
194
195    def __init__(self, repository, working_dir):
196        self.working_dir = working_dir
197        self.repository = repository
198
199    def parse(self, revision):
200        from datetime import datetime
201
202        self.revision=""
203        self.ancestors=[]
204        self.authors=[]
205        self.dates=[]
206        self.changelog=""
207        self.branches=[]
208
209        cmd = self.repository.command("log", "--db", self.repository.repository,
210                                      "--last", "1", "--revision", revision)
211        mtl = ExternalCommand(cwd=self.working_dir, command=cmd)
212        outstr = mtl.execute(stdout=PIPE, stderr=PIPE)
213        if mtl.exit_status:
214            raise GetUpstreamChangesetsFailure("mtn log returned status %d" % mtl.exit_status)
215
216        logs = ""
217        comments = ""
218        state = self.SINGLE
219        loglines = outstr[0].getvalue().splitlines()
220        for curline in loglines:
221
222            pr = self.PrefixRemover(curline)
223            if pr("Revision:"):
224                if pr.value != revision:
225                    raise GetUpstreamChangesetsFailure(
226                        "Revision doesn't match. Expected %s, found %s" % (revision, pr.value))
227                state = self.SINGLE
228            elif pr("Ancestor:"):
229                if pr.value:
230                    self.ancestors.append(pr.value) # cset could be a merge and have multiple ancestors
231                state = self.SINGLE
232            elif pr("Author:"):
233                self.authors.append(pr.value)
234                state = self.SINGLE
235            elif pr("Date:"):
236                    # monotone dates are expressed in ISO8601, always UTC
237                    dateparts = pr.value.split('T')
238                    assert len(dateparts) >= 2, `dateparts`
239                    day = dateparts[0]
240                    time = dateparts[1]
241                    y,m,d = map(int, day.split(day[4]))
242                    hh,mm,ss = map(int, time.split(':'))
243                    date = datetime(y,m,d,hh,mm,ss,0,UTC)
244                    self.dates.append(date)
245                    state = self.SINGLE
246            elif pr("Branch:"):
247                # branch data
248                self.branches.append(pr.value)
249                state = self.SINGLE
250            elif pr("Tag"):
251                # unused data, just resetting state
252                state = self.SINGLE
253            elif pr("Deleted files:") or pr("Deleted directories:") or pr("Deleted entries"):
254                state=self.DEL
255            elif pr("Renamed files:") or pr("Renamed directories:") or pr("Renamed entries"):
256                state=self.REN
257            elif pr("Added files:") or pr("Added directories:"):
258                state=self.ADD
259            elif pr("Modified files:"):
260                state=self.ADD
261            elif pr("ChangeLog:"):
262                state=self.LOG
263            elif pr("Comments:"):
264                comments=comments + "Note:\n"
265                state=self.CMT
266            else:
267                # otherwise, it must be a log/comment/changeset entry, or an unknown cert line
268                if state == self.SINGLE:
269                    # line coming from an unknown cert
270                    pass
271                elif state == self.LOG:
272                    # log line, accumulate string
273                    logs = logs + curline + "\n"
274                elif state == self.CMT:
275                    # comment line, accumulate string
276                    comments = comments + curline + "\n"
277                else:
278                    # parse_cset_entry(mode, chset, curline.strip()) # cset entry, handle
279                    pass # we ignore cset info
280
281        # parsing terminated, verify the data
282        if len(self.authors)<1 or len(self.dates)<1 or revision=="":
283            raise GetUpstreamChangesetsFailure("Error parsing log of revision %s. Missing data" % revision)
284        self.changelog = logs + comments
285
286    def convertLog(self, chset):
287        self.parse(chset.revision)
288
289        chset.update(real_dates=self.dates,
290                     authors=self.authors,
291                     log=self.changelog,
292                     real_ancestors=self.ancestors,
293                     branches=self.branches)
294
295        return chset
296
297
298class MonotoneDiffParser:
299    """
300    This class obtains a diff beetween two arbitrary revisions, parsing
301    it to get changeset entries.
302
303    Note: since monotone tracks directories implicitly, a fake "add dir"
304    cset entry is generated when a file is added to a subdir
305    """
306
307    class BasicIOTokenizer:
308        # To write its control files, monotone uses a format called
309        # internally "basic IO", a stanza file format with items
310        # separated by blank lines. Lines are terminated by newlines.
311        # The format supports strings, sequence of chars contained by
312        # ". String could contain newlines and to insert a " in the
313        # middle you escape it with \ (and \\ is used to obtain the \
314        # char itself) basic IO files are always UTF-8
315        # This class implements a small tokenizer for basic IO
316
317        def __init__(self, stream):
318            self.stream = stream
319
320        def _string_token(self):
321            # called at start of string, returns the complete string
322            # Note: Exceptions checked outside
323            escape = False
324            str=['"']
325            while True:
326                ch = self.it.next()
327                if escape:
328                    escape=False
329                    str.append(ch)
330                    continue
331                elif ch=='\\':
332                    escape=True
333                    continue
334                else:
335                    str.append(ch)
336                    if ch=='"':
337                        break   # end of filename string
338            return "".join(str)
339
340        def _normal_token(self, startch):
341            # called at start of a token, stops at first whitespace
342            # Note: Exceptions checked outside
343            tok=[startch]
344            while True:
345                ch = self.it.next()
346                if ch in whitespace:
347                    break
348                tok.append(ch)
349
350            return "".join(tok)
351
352        def __iter__(self):
353            # restart the iteration
354            self.it = iter(self.stream)
355            return self
356
357        def next(self):
358            token =""
359            while True:
360                ch = self.it.next() # here we just propagate the StopIteration ...
361                if ch in whitespace or ch=='#':
362                    continue  # skip spaces beetween tokens ...
363                elif ch == '"':
364                    try:
365                        token = self._string_token()
366                        break
367                    except StopIteration:
368                        # end of stream reached while in a string: Error!!
369                        raise GetUpstreamChangesetsFailure("diff end while in string parsing.")
370                else:
371                    token = self._normal_token(ch)
372                    break
373            return token
374
375    def __init__(self, repository, working_dir):
376        self.working_dir = working_dir
377        self.repository = repository
378
379    def _addPathToSet(self, s, path):
380        parts = path.split('/')
381        while parts:
382            s.add('/'.join(parts))
383            parts.pop()
384
385    def convertDiff(self, chset):
386        """
387        Fills a chset with the details data coming by a diff between
388        chset lin_ancestor and revision (i.e. the linearized history)
389        """
390        if (not chset.lin_ancestor or
391            not chset.revision or
392            chset.lin_ancestor == chset.revision):
393            raise GetUpstreamChangesetsFailure(
394                "Internal error: MonotoneDiffParser.convertDiff called "
395                "with invalid parameters: lin_ancestor %s, revision %s" %
396                (chset.lin_ancestor, chset.revision))
397
398        # the order of revisions is very important. Monotone gives a
399        # diff from the first to the second
400        cmd = self.repository.command("diff",
401                                      "--db", self.repository.repository,
402                                      "--revision", chset.lin_ancestor,
403                                      "--revision", chset.revision)
404
405        mtl = ExternalCommand(cwd=self.working_dir, command=cmd)
406        outstr = mtl.execute(stdout=PIPE, stderr=PIPE)
407        if mtl.exit_status:
408            raise GetUpstreamChangesetsFailure(
409                "mtn diff returned status %d" % mtl.exit_status)
410
411        # monotone diffs are prefixed by a section containing
412        # metainformations about files
413        # The section terminates with the first file diff, and each
414        # line is prepended by the patch comment char (#).
415        tk = self.BasicIOTokenizer(outstr[0].getvalue())
416        tkiter = iter(tk)
417        in_item = False
418        try:
419            while True:
420                token = tkiter.next()
421                if token.startswith("========"):
422                    # found first patch marker. Changeset info terminated
423                    in_item = False
424                    break
425                else:
426                    in_item = False
427                    # now, next token should be a string or an hash,
428                    # or the two tokens are "no changes"
429                    fname = tkiter.next()
430                    if token == "no" and fname == "changes":
431                        break
432                    elif fname[0] != '"' and fname[0] != '[':
433                        raise GetUpstreamChangesetsFailure(
434                            "Unexpected token sequence: '%s' "
435                            "followed by '%s'" %(token, fname))
436
437                    if token == "content":
438                        pass  # ignore it
439                    # ok, is a file/dir, control changesets data
440                    elif token == "add_file" or token=="add_directory":
441                        chentry = chset.addEntry(fname[1:-1], chset.revision)
442                        chentry.action_kind = chentry.ADDED
443                    elif token=="add_dir":
444                        chentry = chset.addEntry(fname[1:-1], chset.revision)
445                        chentry.action_kind = chentry.ADDED
446                    elif token=="delete":
447                        chentry = chset.addEntry(fname[1:-1], chset.revision)
448                        chentry.action_kind = chentry.DELETED
449                    elif token=="rename":
450                        # renames are in the form:  oldname to newname
451                        tow = tkiter.next()
452                        newname = tkiter.next()
453                        if tow != "to" or fname[0]!='"':
454                            raise GetUpstreamChangesetsFailure(
455                                "Unexpected rename token sequence: '%s' "
456                                "followed by '%s'" %(tow, newname))
457                        chentry = chset.addEntry(newname[1:-1], chset.revision)
458                        chentry.action_kind = chentry.RENAMED
459                        chentry.old_name= fname[1:-1]
460                    elif token == "patch":
461                        # patch entries are in the form: from oldrev to newrev
462                        fromw = tkiter.next()
463                        oldr = tkiter.next()
464                        tow = tkiter.next()
465                        newr = tkiter.next()
466                        if fromw != "from" or tow != "to":
467                            raise GetUpstreamChangesetsFailure(
468                                "Unexpected patch token sequence: '%s' "
469                                "followed by '%s','%s','%s'" % (fromw, oldr,
470                                                                tow, newr))
471
472        except StopIteration:
473            if in_item:
474                raise GetUpstreamChangesetsFailure("Unexpected end of 'diff' parsing changeset info")
475
476
477class MonotoneRevToCset:
478    """
479    This class is used to create changesets from revision ids.
480
481    Since most backends (and tailor itself) don't support monotone
482    multihead feature, sometimes we need to linearize the revision
483    graph, creating syntethized (i.e. fake) edges between revisions.
484
485    The revision itself is real, only its ancestors (and all changes
486    between) are faked.
487
488    To properly do this, changeset are created by a mixture of 'log'
489    and 'diff' output. Log gives the revision data, diff the
490    differences beetween revisions.
491
492    Monotone also supports multiple authors/tags/comments for each
493    revision, while tailor allows only single values.
494
495    We collapse those multiple data (when present) to single entries
496    in the following manner:
497
498    author
499      all entries separated by a comma
500
501    date
502      chooses only one, at random
503
504    changelog
505      all entries appended, without a specific order
506
507    comment
508      all comments are appended to the changelog string, prefixed by a
509      "Note:" line
510
511    tag
512      not used by tailor. Ignored
513
514    branch
515      used to restrict source revs (tailor follows only a single branch)
516
517    testresult
518      ignored
519
520    other certs
521      ignored
522
523    Changesets created by monotone will have additional fields with
524    the original data:
525
526    real_ancestors
527      list of the real revision ancestor(s)
528
529    real_dates
530      list with all date certs
531
532    lin_ancestor
533      linearized ancestor (i.e. previous revision in the linearized history)
534    """
535
536    def __init__(self, repository, working_dir, branch):
537        self.working_dir = working_dir
538        self.repository = repository
539        self.branch = branch
540        self.logparser = MonotoneLogParser(repository=repository,
541                                           working_dir=working_dir)
542        self.diffparser = MonotoneDiffParser(repository=repository,
543                                             working_dir=working_dir)
544
545    def updateCset(self, chset):
546        # Parsing the log fills the changeset from revision data
547        self.logparser.convertLog(chset)
548
549        # if an ancestor is available, fills the cset with file/dir entries
550        if chset.lin_ancestor:
551            self.diffparser.convertDiff(chset)
552
553    def getCset(self, revlist, onlyFirst):
554        """
555        receives a revlist, already toposorted (i.e. ordered by
556        ancestry) and outputs a list of changesets, filtering out revs
557        outside the chosen branch. If onlyFirst is true, only the
558        first valid element is considered
559        """
560        cslist=[]
561        anc=revlist[0]
562        for r in revlist[1:]:
563            chtmp = MonotoneChangeset(anc, r)
564            self.logparser.convertLog(chtmp)
565            if self.branch in chtmp.branches:
566                cslist.append(MonotoneChangeset(anc, r)) # using a new, unfilled changeset
567                anc=r
568                if onlyFirst:
569                    break
570        return cslist
571
572
573class MonotoneWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir):
574
575    def _convert_head_initial(self, dbrepo, module, revision, working_dir):
576        """
577        This method handles HEAD and INITIAL pseudo-revisions, converting
578        them to monotone revids
579        """
580        effective_rev = revision
581        if revision == 'HEAD' or revision=='INITIAL':
582            # in both cases we need the head(s) of the requested branch
583            cmd = self.repository.command("automate","heads",
584                                          "--db", dbrepo, module)
585            mtl = ExternalCommand(cwd=working_dir, command=cmd)
586            outstr = mtl.execute(stdout=PIPE, stderr=PIPE)
587            if mtl.exit_status:
588                raise InvocationError("The branch '%s' is empty" % module)
589
590            revision = outstr[0].getvalue().split()
591            if revision == 'HEAD':
592                if len(revision)>1:
593                    raise InvocationError("Branch '%s' has multiple heads. "
594                                          "Please choose only one." % module)
595                effective_rev=revision[0]
596            else:
597                # INITIAL requested. We must get the ancestors of
598                # current head(s), topologically sort them and pick
599                # the first (i.e. the "older" revision). Unfortunately
600                # if the branch has multiple heads then we could end
601                # up with only part of the ancestry graph.
602                if len(revision)>1:
603                    self.log.info('Branch "%s" has multiple heads. There '
604                                  'is no guarantee to reconstruct the '
605                                  'full history.', module)
606                cmd = [ self.repository.command("automate","ancestors",
607                                                "--db",dbrepo),
608                        self.repository.command("automate","toposort",
609                                                "--db",dbrepo, "-@-")
610                        ]
611                cmd[0].extend(revision)
612                cld = ExternalCommandChain(cwd=working_dir, command=cmd)
613                outstr = cld.execute()
614                if cld.exit_status:
615                    raise InvocationError("Ancestor reading returned "
616                                          "status %d" % cld.exit_status)
617                revlist = outstr[0].getvalue().split()
618                if len(revlist)>1:
619                    mtr = MonotoneRevToCset(repository=self.repository,
620                                            working_dir=working_dir,
621                                            branch=module)
622                    first_cset = mtr.getCset(revlist, True)
623                    if len(first_cset)==0:
624                        raise InvocationError("Can't find an INITIAL revision on branch '%s'."
625                                              % module)
626                    effective_rev=first_cset[0].revision
627                else:
628                    effective_rev=revlist[0]
629        return effective_rev
630
631    ## UpdatableSourceWorkingDir
632
633    def _getUpstreamChangesets(self, sincerev=None):
634        # mtn descendents returns results sorted in alpha order
635        # here we want ancestry order, so descendents output is feed back to
636        # mtn for a toposort ...
637        cmd = [ self.repository.command("automate","descendents",
638                                        "--db", self.repository.repository,
639                                        sincerev),
640                self.repository.command("automate","toposort",
641                                        "--db", self.repository.repository,
642                                        "-@-")
643                ]
644        cld = ExternalCommandChain(cwd=self.repository.rootdir, command=cmd)
645        outstr = cld.execute()
646        if cld.exit_status:
647            raise InvocationError("mtn descendents returned "
648                                  "status %d" % cld.exit_status)
649
650        # now childs is a list of revids, we must transform it in a
651        # list of monotone changesets. We fill only the
652        # linearized ancestor and revision ids, because at this time
653        # we need only to know WICH changesets must be applied to the
654        # target repo, not WHAT are the changesets (apart for filtering
655        # the outside-branch revs)
656        childs = [sincerev] +outstr[0].getvalue().split()
657        mtr = MonotoneRevToCset(repository=self.repository,
658                                working_dir=self.repository.rootdir,
659                                branch=self.repository.module)
660        chlist = mtr.getCset(childs, False)
661        return chlist
662
663    def _applyChangeset(self, changeset):
664        cmd = self.repository.command("update", "--revision", changeset.revision)
665        mtl = ExternalCommand(cwd=self.repository.basedir, command=cmd)
666        mtl.execute(stdout=PIPE, stderr=PIPE)
667        if mtl.exit_status:
668            raise ChangesetApplicationFailure("'mtn update' returned "
669                                              "status %s" % mtl.exit_status)
670        mtr = MonotoneRevToCset(repository=self.repository,
671                                working_dir=self.repository.basedir,
672                                branch=self.repository.module)
673        mtr.updateCset( changeset )
674
675        return False   # no conflicts
676
677    def _checkoutUpstreamRevision(self, revision):
678        """
679        Concretely do the checkout of the FIRST upstream revision.
680        """
681        effrev = self._convert_head_initial(self.repository.repository,
682                                           self.repository.module, revision,
683                                           self.repository.rootdir)
684        if not exists(join(self.repository.basedir, '_MTN')):
685
686            # actually check out the revision
687            self.log.info("Checking out a working copy")
688            cmd = self.repository.command("co",
689                                          "--db", self.repository.repository,
690                                          "--revision", effrev,
691                                          "--branch", self.repository.module,
692                                          self.repository.basedir)
693            mtl = ExternalCommand(cwd=self.repository.rootdir, command=cmd)
694            mtl.execute(stdout=PIPE, stderr=PIPE)
695            if mtl.exit_status:
696                raise TargetInitializationFailure(
697                    "'mtn co' returned status %s" % mtl.exit_status)
698        else:
699            self.log.debug("%r already exists, assuming it's a monotone "
700                           "working dir already populated", self.repository.basedir)
701
702        # Ok, now the workdir contains the checked out revision. We
703        # need to return a changeset describing it.  Since this is the
704        # first revision checked out, we don't have a (linearized)
705        # ancestor, so we must use None as the lin_ancestor parameter
706        chset = MonotoneChangeset(None, effrev)
707
708        # now we update the new chset with basic data - without the
709        # linearized ancestor, changeset entries will NOT be filled
710        mtr = MonotoneRevToCset(repository=self.repository,
711                                working_dir=self.repository.basedir,
712                                branch=self.repository.module)
713        mtr.updateCset(chset)
714        return chset
715
716    ## SynchronizableTargetWorkingDir
717
718    def _addPathnames(self, names):
719        """
720        Add some new filesystem objects, skipping directories.
721        In monotone *explicit* directory addition is always recursive,
722        so adding a directory here might interfere with renames.
723        Adding files without directories doesn't cause problems,
724        because adding a file implicitly adds the parent directory
725        (non-recursively).
726        """
727        fnames=[]
728        for fn in names:
729            if isdir(join(self.repository.basedir, fn)):
730                self.log.debug("ignoring addition of directory %r "
731                               "(dirs are implicitly added by files)", fn)
732            else:
733                fnames.append(fn)
734        if len(fnames):
735            # ok, we still have something to add
736            cmd = self.repository.command("add")
737            add = ExternalCommand(cwd=self.repository.basedir, command=cmd)
738            add.execute(fnames, stdout=PIPE, stderr=PIPE)
739            if add.exit_status:
740                raise ChangesetApplicationFailure("%s returned status %s" %
741                                                    (str(add),add.exit_status))
742
743    def _addSubtree(self, subdir):
744        """
745        Add a whole subtree (recursively)
746        """
747        cmd = self.repository.command("add", "--recursive")
748        add = ExternalCommand(cwd=self.repository.basedir, command=cmd)
749        add.execute(subdir, stdout=PIPE, stderr=PIPE)
750        if add.exit_status:
751            raise ChangesetApplicationFailure("%s returned status %s" %
752                                              (str(add),add.exit_status))
753
754    def _commit(self, date, author, patchname, changelog=None, entries=None):
755        """
756        Commit the changeset.
757        """
758
759        encode = self.repository.encode
760
761        logmessage = []
762        if patchname:
763            logmessage.append(patchname)
764        if changelog:
765            logmessage.append(changelog)
766
767        rontf = ReopenableNamedTemporaryFile('mtn', 'tailor')
768        log = open(rontf.name, "w")
769        log.write(encode('\n'.join(logmessage)))
770        log.close()
771
772        date = date.astimezone(UTC).replace(tzinfo=None) # monotone wants UTC
773        cmd = self.repository.command("commit",
774                                      "--author", encode(author),
775                                      "--date", date.isoformat(),
776                                      "--message-file", rontf.name)
777        commit = ExternalCommand(cwd=self.repository.basedir, command=cmd)
778
779        entries = None
780        if not entries:
781            entries = ['.']
782
783        output, error = commit.execute(entries, stdout=PIPE, stderr=PIPE)
784
785        # monotone complaints if there are no changes from the last commit.
786        # we ignore those errors ...
787        if commit.exit_status:
788            text = error.read()
789            if not "mtn: misuse: no changes to commit" in text:
790                self.log.error("Monotone commit said: %s", text)
791                raise ChangesetApplicationFailure(
792                    "%s returned status %s" % (str(commit),commit.exit_status))
793            else:
794                self.log.info("No changes to commit - changeset ignored")
795
796    def _removePathnames(self, names):
797        """
798        Remove some filesystem object.
799        """
800
801        cmd = self.repository.command("drop", "--recursive")
802        drop = ExternalCommand(cwd=self.repository.basedir, command=cmd)
803        dum, error = drop.execute(names, stdout=PIPE, stderr=PIPE)
804        if drop.exit_status:
805            errtext = error.read()
806            self.log.error("Monotone drop said: %s", errtext)
807            raise ChangesetApplicationFailure("%s returned status %s" %
808                                                  (str(drop),
809                                                   drop.exit_status))
810
811    def _renamePathname(self, oldname, newname):
812        """
813        Rename a filesystem object.
814        """
815        cmd = self.repository.command("rename")
816        rename = ExternalCommand(cwd=self.repository.basedir, command=cmd)
817        rename.execute(oldname, newname, stdout=PIPE, stderr=PIPE)
818        if rename.exit_status:
819            raise ChangesetApplicationFailure(
820                     "%s returned status %s" % (str(rename),rename.exit_status))
821
822    def _prepareTargetRepository(self):
823        """
824        Check for target repository existence, eventually create it.
825        """
826
827        self.repository.create()
828
829    def _prepareWorkingDirectory(self, source_repo):
830        """
831        Possibly checkout a working copy of the target VC, that will host the
832        upstream source tree, when overriden by subclasses.
833        """
834
835        from re import escape
836
837        if not self.repository.repository or exists(join(self.repository.basedir, '_MTN')):
838            return
839
840        if not self.repository.module:
841            raise TargetInitializationFailure("Monotone needs a module "
842                                              "defined (to be used as "
843                                              "commit branch)")
844
845
846        cmd = self.repository.command("setup",
847                                      "--db", self.repository.repository,
848                                      "--branch", self.repository.module)
849
850        if self.repository.keygenid:
851           self.repository.keyid = self.repository.keygenid
852        if self.repository.keyid:
853            cmd.extend( ("--key", self.repository.keyid) )
854
855        setup = ExternalCommand(command=cmd)
856        setup.execute(self.repository.basedir, stdout=PIPE, stderr=PIPE)
857
858        if self.repository.passphrase or self.repository.custom_lua:
859            monotonerc = open(join(self.repository.basedir, '_MTN', 'monotonerc'), 'w')
860            if self.repository.passphrase:
861                monotonerc.write(MONOTONERC % self.repository.passphrase)
862            else:
863                raise TargetInitializationFailure("The passphrase must be specified")
864            if self.repository.custom_lua:
865                self.log.info("Adding custom lua script")
866                monotonerc.write(self.repository.custom_lua)
867            monotonerc.close()
868
869        # Add the tailor log file and state file to _MTN's list of
870        # ignored files
871        ignored = []
872        logfile = self.repository.projectref().logfile
873        if logfile.startswith(self.repository.basedir):
874            ignored.append('^%s$' %
875                           escape(logfile[len(self.repository.basedir)+1:]))
876
877        sfname = self.repository.projectref().state_file.filename
878        if sfname.startswith(self.repository.basedir):
879            sfrelname = sfname[len(self.repository.basedir)+1:]
880            ignored.append('^%s$' % escape(sfrelname))
881            ignored.append('^%s$' % escape(sfrelname + '.old'))
882            ignored.append('^%s$' % escape(sfrelname + '.journal'))
883
884        if len(ignored) > 0:
885            mt_ignored = open(join(self.repository.basedir, '.mtn-ignore'), 'aU')
886            mt_ignored.write('\n'.join(ignored))
887            mt_ignored.close()
888
889    def _initializeWorkingDir(self):
890        """
891        Setup the monotone working copy
892
893        The user must setup a monotone working directory himself or use the
894        tailor config file to provide parameters for creation. Then
895        we simply use 'mtn commit', without having to specify a database
896        file or branch. Monotone looks up the database and branch in it's _MTN
897        directory.
898        """
899
900        if not exists(join(self.repository.basedir, '_MTN')):
901            raise TargetInitializationFailure("Please setup '%s' as a "
902                                              "monotone working directory" %
903                                              self.repository.basedir)
904
905        SynchronizableTargetWorkingDir._initializeWorkingDir(self)
Note: See TracBrowser for help on using the repository browser.