source: tailor/vcpx/monotone.py @ 1029

Revision 1029, 37.3 KB checked in by R.Ghetta <birrachiara@…>, 7 years ago (diff)

use lelit method to distinguish between 2.3 Set and 2.4 set

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