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

Revision 1302, 35.9 KB checked in by elb@…, 6 years ago (diff)

Monotone add is no longer recursive by default (as of 2006-11-02).
Use add --recursive when adding subtrees.

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