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

Revision 1385, 40.1 KB checked in by henry.ne@…, 6 years ago (diff)

monotone-testresult-text-fuzz.patch

Text fuzz changes
and testing to´learn darcs using for sending patches

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