source: tailor/vcpx/monotone.py @ 941

Revision 941, 34.6 KB checked in by lele@…, 8 years ago (diff)

Fix a typo

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