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

Revision 1645, 43.2 KB checked in by lele@…, 5 years ago (diff)

Clean up the code

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