source: pyutilib.svn/trunk/pyutilib/svn/external_manager.py @ 2832

Revision 2832, 61.2 KB checked in by wehart, 2 years ago (diff)

Portability fixes for Python 3.x

Line 
1#!/usr/bin/env python
2#  _________________________________________________________________________
3#
4#  PyUtilib: A Python utility library.
5#  Copyright (c) 2008 Sandia Corporation.
6#  This software is distributed under the BSD License.
7#  Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
8#  the U.S. Government retains certain rights in this software.
9#  _________________________________________________________________________
10#
11import os
12import re
13import sys
14import yaml
15from copy import deepcopy
16from pyutilib.svn.core import log, SvnError, VerifySvnVersion
17from pyutilib.svn.repository import Repository, svn_info, svn_externals, \
18     svn_propget, externals_14link_re, externals_15link_re
19from pyutilib.svn.database import DatabaseError, RepositoryDatabase
20from subprocess import Popen, PIPE
21
22class ExternalError(Exception):
23    """Exception raised due to tracing error."""
24
25def clean_url(a):
26    return '://'.join([re.sub('//+','/',x) for x in a.strip('/').split('://')])
27
28illegal_characters = "(){}[]\\ \t\n"
29
30
31class ResolvedRepo(object):
32    def __init__(self, repo):
33        self.raw     = repo
34        self.url     = repo.url
35        self.rev     = repo.revision
36        self.projects = {} # map of Project name -> ResolvedProject
37
38class ResolvedProject(object):
39    def __init__(self, project, repo):
40        self.raw     = project
41        self.name    = project.name
42        self.url     = project.url
43        self.rev     = project.revision
44        self.repo    = repo
45        self.targets = {} # map of Target name -> ResolvedTarget
46
47class ResolvedTarget(object):
48    def __init__(self, target, project):
49        self.raw       = target
50        self.name      = target.name
51        self.url       = target.url
52        self.rev       = target.revision
53        self.project   = project
54        self.externals = {} # map of external.path -> ResolvedExternal
55
56    def __str__(self):
57        return self.project.name + " % " + str(self.name)
58
59class ResolvedExternal(object):
60    def __init__(self, raw, target, url, deep):
61        self.raw      = raw
62        self.url      = clean_url(url + '/' + deep)
63        self.rev      = raw.revision
64        self.path     = raw.path
65        self.target   = target
66        if target is None:
67            self.deep_ref = None
68        else:
69            self.deep_ref = deep
70
71class Description_External(object):
72    def __init__(self, path, link=None, project=None):
73        if not ( bool(link is None) ^ bool(project is None) ):
74            raise ExternalError("Description_External: cannot specify "
75                                "both link and project")
76        self.path = path
77        self.link = link
78        self.project = project
79        self.target = None
80        self.deep_ref = None
81        self.rev = None
82
83    def name(self):
84        return self.project is not None and self.project or self.link
85
86    def url(self, em, repo):
87        if self.link is not None:
88            a = self.link
89        else:
90            p = em.get_project(self.project, repo)
91            if p is None:
92                log.warning("Unable to uniquely identify project %s" %
93                            self.project)
94                a = self.project + '/' + self.target
95            else:
96                a = p.url + '/' + self.target
97        return clean_url(a + '/' + (self.deep_ref or ""))
98
99    def __str__(self):
100        if self.link is not None:
101            s = self.link
102        else:
103            s = self.project
104            if self.target is not None:
105                s += " % " + self.target
106            if self.deep_ref is not None:
107                s += " > " + self.deep_ref
108        if self.rev is not None:
109            s += " @ " + str(self.rev)
110        return s.strip()
111
112    @staticmethod
113    def parse_from(path, info, em, repo):
114        seps = {}
115        for sep in ['%', '>', '@']:
116            i = info.find(sep)
117            if i >= 0:
118                seps[i] = sep
119        rev = deep = target = None
120        for index, sep in sorted(seps.items(), reverse=True):
121            fields = info.split(sep)
122            if len(fields) > 2:
123                raise ExternalError("ERROR: bad external syntax")
124            if sep == '@':
125                rev = int(fields[1])
126            elif sep == '>':
127                deep = fields[1].strip()
128            elif sep == '%':
129                target = fields[1].strip()
130            info = fields[0].strip()
131        info = info.strip()
132        if len(info) == 0 or em.get_project(info, repo) is not None:
133           
134            e = Description_External(path, project=info)
135        else:
136            e = Description_External(path, link=info)
137        e.rev = rev
138        e.deep_ref = deep
139        e.target = target
140        return e
141
142class Description_Retarget(object):
143    def __init__(self, target, rev=None):
144        self.target = target
145        self.rev = rev
146
147    def __eq__(this, that):
148        return that and this.target == that.target and this.rev == that.rev
149
150    def __str__(self):
151        if self.target:
152            s = self.target
153        else:
154            s = ''
155        if self.rev is not None:
156            s += " @ " + str(self.rev)
157        return s.strip()
158
159    @staticmethod
160    def parse_from(path, info):
161        fields = info.split('@')
162        if len(fields) > 2:
163            raise ExternalError("ERROR: bad retarget syntax")
164        return Description_Retarget( fields[0].strip(),
165                                     len(fields)>1 and int(fields[1]) or None )
166
167class Description_Inherit(object):
168    def __init__(self, target, project=None):
169        self.target = target
170        self.project = project
171
172    def __eq__(this, that):
173        return that \
174            and this.target == that.target \
175            and this.project == that.project
176
177    def __str__(self):
178        if self.project is not None:
179            s = self.project + " % " + self.target
180        else:
181            s = self.target
182        return s.strip()
183
184    @staticmethod
185    def parse_from(info):
186        fields = info.split('%')
187        if len(fields) > 2:
188            raise ExternalError("ERROR: bad inherit syntax")
189        if len(fields) == 1:
190            fields.insert(0,None)
191        else:
192            fields[0] = fields[0].strip()
193        return Description_Inherit( fields[1].strip(), fields[0] )
194
195url_re = re.compile('([^/]+://)?(.*)')
196
197class Status:
198    ok          = ''
199    aliased_url = '~'
200    excluded    = '>'
201    non_target  = '?'
202    dne_in_head = '-'
203    broken      = 'X'
204    not_pegged  = 'F'
205    out_of_date = '<'
206
207    @staticmethod
208    def print_key():
209        print("""Key to integrity error codes:
210  > : external points to a repository excluded from analysis by configuration
211  ~ : external references target through an alias (e.g. svn+ssh instead of https)
212  ? : external points to a location not affiliated with a known target
213  X : external is broken and unresolvable
214  - : external points to a valid pegged location that does not exist in HEAD
215  F : external is floating (not pegged), but referenced through a pegged target
216  < : external is pegged at a revision earlier than HEAD
217""")
218
219
220class ExternalManager(object):
221    def __init__(self, config=None):
222        self.all_repos    = [] # List of ResolvedRepo
223        self.all_targets  = {} # map of target url -> ResolvedTarget
224        self.all_projects = {} # map of project url -> ResolvedProject
225        self.projects     = {} # map of project name -> ResolvedProject;
226            # could be None if multiple projects by the same name and no
227            # PromaryProject is declared.
228           
229        self.config       = {}
230
231        # Path to the repository database cache file
232        self.config['repo_db'] = None # <- use default db
233
234        # list of URLs to exclude (treat as truly external and do not
235        # add them to the repository database)
236        self.config['exclude'] = []
237
238        # Sometimes the same repository can be accessed through multiple
239        # URLs.  This tells which URLs can be treated as an alias of the
240        # "real" URL.  Each entry in the list is a (alias URL, real URL)
241        # tuples.
242        self.config['aliases']          = []
243
244        # Sometimes there are multiple *different* projects that use the
245        # same name.  This tells the ExternalManager which one we will
246        # usually use (and can be referenced through the project name).
247        # All non-primary projects will have to use full URL references.
248        #
249        # This is a map of "project_name -> repo_URL:project_name".
250        # Note: if the repository *is* the project (that is, has a
251        # top-level trunk directory), the map value *must* end with a
252        # ":" (to define a "" project name)
253        self.config['primary_projects'] = {}
254
255        # The list of default targets that should have pegged revision numbers
256        self.config['pegged_targets']   = ['releases', 'tags', 'maintenance']
257
258        # directory for instantiating changeset
259        self.config['workingdir']       = 'svnpm-tmp'
260
261        if not config:
262            config = ExternalManager.default_config()
263        if isinstance(config, dict):
264            self.config.update(config)
265        elif os.path.exists(config):
266            self.load_config(config)
267
268        self.db = RepositoryDatabase(self.config['repo_db'])
269        try:
270            self.db.load()
271        except DatabaseError:
272            log.info("Database file DNE; initializing empty database")
273
274    @staticmethod
275    def default_config():
276        return os.environ["HOME"]+os.sep+".pyutilib.svn"+os.sep+'config.yaml'
277
278    def get_repository(self, repo_name):
279        for r in self.all_repos:
280            if r.url == repo_name:
281                return r
282        if repo_name in self.all_projects:
283            return self.all_projects[repo_name].repo
284        if repo_name in self.projects:
285            return self.projects[repo_name].repo
286        return None
287
288    def get_project(self, project_name, repo=None):
289        if project_name in self.all_projects:
290            return self.all_projects[project_name]
291        elif project_name in self.projects:
292            proj = self.projects[project_name]
293            if proj is None and repo is not None:
294                if project_name in repo.projects:
295                    if repo.projects[project_name] is not None:
296                        return repo.projects[project_name]
297            #log.warning("Looking up project '%s', but there are multiple "
298            #            "projects with that name." % (project_name,))
299            return proj
300        else:
301            return None
302
303    def load_config(self, fname):
304        if not os.path.exists( fname ):
305            raise ExternalError("Configuration file %s does not exist" % fname)
306        log.info("Reading configuration from '%s'" % fname)
307        INPUT=open(fname)
308        config = yaml.load(INPUT)
309        INPUT.close()
310        self.config.update(config)
311
312    def update(self):
313        """Update all repository caches.  Also, look for any externals
314        pointing to new repositories and add those repositories to the
315        cache."""
316        def _locate_external_repo(self, external, known, pending):
317            url = external.url
318            if not url:
319                raise ExternalError( "bad external: %s" % str(external) )
320            for alias, main in self.config['aliases']:
321                if url.startswith(alias):
322                    url = url.replace(alias, main, 1)
323            for e in self.config['exclude']:
324                if url.startswith(e):
325                    return
326            for r in known:
327                if url.startswith(r):
328                    return
329
330            # Now it is likely that this is a url to a new repo --
331            # figure out what the repo root url is
332            protocol, url = url_re.match(url).groups()
333            while len(url):
334                try:
335                    tmp = Repository((protocol or "")+url, external.revision)
336                    if tmp.url not in known:
337                        log.info("external (%s)\nreferences new repo %s" % \
338                                 ( external.url, tmp.url ))
339                        known.append(tmp.url)
340                        new_repo = self.db.add(tmp.url)
341                        pending.insert(0, new_repo)
342
343                        # We save things as we go so aborting early
344                        # doesn't lose work
345                        self.db.save()
346                    break
347                except SvnError:
348                    try:
349                        (url,junk) = url.rsplit('/',1)
350                    except ValueError:
351                        return # it's OK not to find a repo --
352                               # broken links will be handled later
353        # { end def _validate_external_repo() }
354
355        # First, update all the repos we know about
356        if self.db.update():
357            # We save things as we go so aborting early doesn't lose work
358            self.db.save()
359
360        # We now need to make sure we have loaded and indexed all the
361        # repositories we will need
362        known_repos = self.db.repos.keys()
363        pending_repos = self.db.repos.values()
364        while pending_repos:
365            repo = pending_repos.pop()
366            for project in repo.projects.itervalues():
367                for target in project.targets.itervalues():
368                    for external in target.externals:
369                        _locate_external_repo\
370                            ( self, external, known_repos, pending_repos )
371
372
373    def resolve_targets(self):
374        """Converts the simple tree (DAG) representation of the
375        repository managed by the repo database into a fully
376        cross-linked repository graph."""
377
378        # Pass 1: resolve all the projects and targets
379        for repo in self.db.repos.itervalues():
380            r = ResolvedRepo(repo)
381            self.all_repos.append(r)
382            for project in repo.projects.itervalues():
383                if project.name in self.projects and \
384                       project.name not in self.config['primary_projects']:
385                    tmp = self.projects[project.name]
386                    log.warning("duplicate project (%s)\n[new ] %s\n[main] %s"
387                                % ( project.name, project.url,
388                                    tmp and tmp.url or None ))
389                    self.projects[project.name] = None
390                p = self.all_projects[project.url] = ResolvedProject(project,r)
391                self.projects.setdefault(project.name, p)
392                r.projects.setdefault(project.name, p)
393                for target in project.targets.itervalues():
394                    if target.url in self.all_targets:
395                        raise ExternalError( "(ERROR): duplicate target URL "
396                                             "found (%s)" % target.url )
397                    self.all_targets[target.url] \
398                            = p.targets[target.name] \
399                            = ResolvedTarget(target, p)
400
401
402        # sanity check the duplicate project names, and override the
403        # name resolution
404        for name, project in self.config['primary_projects'].iteritems():
405            (repo, proj) = project.rsplit(':', 1)
406            if repo not in self.db.repos:
407                log.error("primary_projects: unknown repository (%s)" % repo)
408            elif proj not in self.db.repos[repo].projects:
409                log.error("primary_projects: unknown project (%s)" % project)
410            else:
411                self.projects[name] =  self.all_projects[ \
412                    self.db.repos[repo].projects[proj].url ]
413
414        # Pass 2: try and resolve all the external urls to Target objects
415        for target in self.all_targets.itervalues():
416            for external in target.raw.externals:
417                url = external.url
418                for alias, main in self.config['aliases']:
419                    if url.startswith(alias):
420                        url = url.replace(alias, main, 1)
421                deepRef = ''
422                while len(url):
423                    t = self.all_targets.get(url, None)
424                    if t is not None:
425                        break
426                    if url in self.db.repos:
427                        break
428                    try:
429                        (url, tmp) = url.rsplit('/',1)
430                    except ValueError:
431                        tmp = url
432                        url = ""
433                    deepRef = tmp + '/' + deepRef
434                # ...We won't complain about broken externals (yet)
435                #if e is None:
436                #    raise ExternalError( "target (%s) not found" %
437                #                         str(e) )
438                target.externals[external.path] = \
439                    ResolvedExternal(external, t, url, deepRef)
440
441
442    def check_integrity(self, repo=None, project=None, key=None):
443        """Perform a basic integrity check on the repositories.  This
444        will identify common errors like:
445          - broken externals.
446          - externals that point through an aliased URL.
447          - externals that point to an excluded URL.
448          - externals that point to a location other than a known
449            project target.
450          - pegged externals that work but point to locations that no
451            longer exist at the HEAD."""
452
453        def _integrity_error(target, external, error):
454            return "%s  %-15s : %-25s (%s)" % \
455                  ( error, target.project.name,
456                    target.name, str(external.raw) )
457
458        if repo is not None and project is not None:
459            if repo == project.repo:
460                repo = None
461            else:
462                raise ExternalError("(ERROR) check_integrity: cannot specify "
463                                    "both repo and project")
464        if key:
465            Status.print_key()
466
467        if project is not None:
468            projects = [ project ]
469        elif repo is not None:
470            projects = sorted( repo.projects.values(),
471                               cmp=lambda x,y: cmp(x.url, y.url) )
472        else:
473            projects = sorted( self.all_projects.values(),
474                               cmp=lambda x,y: cmp(x.url, y.url) )
475
476        for p in projects:
477            targets = sorted( p.targets.values(),
478                              cmp=lambda x,y: cmp(x.name, y.name) )
479            for t in targets:
480                for e in t.externals.itervalues():
481                    error, rev = self._resolve_external(e)
482                    if error:
483                        print(_integrity_error(t, e, error))
484
485
486    def check_pegged_target(self, target, peg=None, parent=''):
487        """Utility function that performs the check_pegged() on a
488        single target."""
489
490        def _peg_error(path, e, error, rev=None):
491            return "%s  %-30s %-8s: %s%s" % \
492                   ( error, path, rev and (": %s" % rev) or "", e.url,
493                     e.rev and (" @ %s" % e.rev) or "" )
494
495        if peg is None:
496            for x in self.config['pegged_targets']:
497                if target.name.startswith(x):
498                    peg = target.rev
499                    break
500
501        ans = []
502        for e in target.externals.itervalues():
503            if parent and e.path:
504                path = parent + '/' + e.path
505            else:
506                path = parent + e.path
507
508            error, rev = self._resolve_external(e)
509            if rev and e.rev and e.rev < rev:
510                ans.append( _peg_error(path, e, Status.out_of_date, rev) )
511            elif peg is not None:
512                if e.rev is None:
513                    ans.append( _peg_error(path, e, Status.not_pegged) )
514                elif error and error != Status.dne_in_head:
515                    ans.append( _peg_error(path, e, error) )
516            elif error and error != Status.dne_in_head:
517                ans.append( _peg_error(path, e, error) )
518
519            if e.target is not None:
520                ans.extend(self.check_pegged_target(e.target, e.rev, path))
521        return ans
522
523    def check_pegged(self, repo=None, project=None, key=None):
524        """Perform a check for pegged externals in the repositories.  In
525        addition to basic integrity errors identified by
526        check_integrity, this will identify errors like:
527
528          - externals that are pegged to revisions older than HEAD for
529            that location.
530          - externals that should be pegged, but are not."""
531
532        if repo is not None and project is not None:
533            if repo == project.repo:
534                repo = None
535            else:
536                raise ExternalError("(ERROR) check_pegged: cannot specify "
537                                    "both repo and project")
538        if key:
539            Status.print_key()
540
541        if project is not None:
542            print_header = True
543            targets = sorted( project.targets.values(),
544                              cmp=lambda x,y: cmp(x.name, y.name) )
545            for t in targets:
546                ans = self.check_pegged_target(t)
547                if len(ans):
548                    if print_header:
549                        print(project.url)
550                        print_header = False
551                    print("   %s" % t.name)
552                    print("      %s" % ( '\n      '.join(ans) ))
553            return
554
555
556        repos = repo is None and self.all_repos or [ repo ]
557        for repo in repos:
558            for p in sorted( repo.projects.values(),
559                             cmp=lambda x,y: cmp(x.url, y.url) ):
560                print_header = True
561                for t in sorted( p.targets.values(),
562                                 cmp=lambda x,y: cmp(x.name, y.name) ):
563                    ans = self.check_pegged_target(t)
564                    if len(ans):
565                        if print_header:
566                            print(p.url)
567                            print_header = False
568                        print("   %s" % t.name)
569                        print("      %s" % ( '\n      '.join(ans) ))
570
571    def print_status(self, repo=None, project=None):
572        """Print the status of the project/repository."""
573
574        if repo is not None and project is not None:
575            if repo == project.repo:
576                repo = None
577            else:
578                raise ExternalError("(ERROR) print_status: cannot specify "
579                                    "both repo and project")
580
581        maxRev = 0
582        maxLen = 0
583
584        if project is not None:
585            print_header = True
586            targets = sorted( project.targets.values(),
587                              cmp=lambda x,y: cmp(x.name, y.name) )
588            for t in targets:
589                maxRev = max(maxRev, t.rev)
590                maxLen = max(maxLen, len(t.name))
591            field = "   %%-%is %%1s%%s" % maxLen
592            for t in targets:
593                if print_header:
594                    print(project.url)
595                    print_header = False
596                print(field % ( t.name, t.rev==maxRev and '*' or '', t.rev ))
597            return
598
599        repos = repo is None and self.all_repos or [ repo ]
600        for repo in repos:
601            for p in sorted( repo.projects.values(),
602                             cmp=lambda x,y: cmp(x.url, y.url) ):
603                maxRev = maxLen = 0
604                for t in p.targets.itervalues():
605                    maxRev = max(maxRev, t.rev)
606                    maxLen = max(maxLen, len(t.name))
607                field = "   %%-%is\t%%1s%%s" % maxLen
608                print_header = True
609                for t in sorted( p.targets.values(),
610                                 cmp=lambda x,y: cmp(x.name, y.name) ):
611                    if print_header:
612                        print(p.url)
613                        print_header = False
614                    print(field % ( t.name, t.rev==maxRev and '*' or '', t.rev ))
615
616    def find_references(self, url):
617        ans = []
618        for tUrl, t in self.all_targets.iteritems():
619            for e in t.externals.itervalues():
620                if e.url.startswith(url):
621                    ans.append((t.project.name, t.name))
622                    break
623        return ans
624
625    def reference_map(self):
626        ans = {}
627        for tUrl, t in self.all_targets.iteritems():
628            refs = self.find_references(tUrl)
629            if refs:
630                ans[t] = refs
631        return ans
632
633    def load_repo_definition(self, definition_fname):
634        if not os.path.exists( definition_fname ):
635            raise ExternalError("Repository definition file %s does not exist" %
636                                definition_fname)
637        log.info("Reading repository definition from '%s'" % definition_fname)
638        FILE = open(definition_fname)
639        defn = yaml.load(FILE)
640        FILE.close()
641        self.expand_repo_definition(defn)
642        return defn
643
644    def generate_repo_definition(self, repo=None, proj=None):
645        """Generate a definition dictionary for a single repository,
646        based on the current status of that repository.  This attempts
647        to identify and leverage common inheritance patterns used in
648        repositories in order to reduce the size and complexity of the
649        definition."""
650
651        def _reduce_pegged(target):
652            pass
653            #targetExternals = target.get('externals', {})
654            #for eName, e in targetExternals.iteritems():
655            #    if e.target:
656            #        retarget = Description_Retarget(e.target, e.rev)
657            #        if target.setdefault('retarget', {}).setdefault \
658            #                ( e.name(), retarget ) == retarget:
659            #            e.target = None
660            #            e.rev = None
661        # { END def _reduce_pegged() }
662
663        def _reduce_inherited(trunkExternals, target):
664            modified = False
665            targetExternals = target.get('externals', {})
666            for eName, e in trunkExternals.iteritems():
667                tmpE = targetExternals.get(eName, None)
668                if tmpE is None or tmpE.name() != e.name() or \
669                        tmpE.deep_ref != e.deep_ref:
670                    target.setdefault('remove',[]).append(eName)
671                    modified = True
672                elif tmpE.target != e.target or tmpE.rev != e.rev:
673                    retarget = Description_Retarget(tmpE.target, tmpE.rev)
674                    if target.setdefault('retarget', {}).setdefault \
675                            ( tmpE.name(), retarget ) == retarget:
676                        del targetExternals[eName]
677                        modified = True
678                else:
679                    del targetExternals[eName]
680
681            if modified:
682                target['inherit'] = [ Description_Inherit('trunk') ]
683        # { END def _reduce_inherited() }
684
685
686        if not ((repo is None) ^ (proj is None)):
687            raise ExternalError("ERROR: cannot specify both repo and proj")
688
689        if repo is not None:
690            projects = repo.projects.keys()
691        else:
692            projects = [ proj.name ]
693            repo = proj.repo
694
695        ans = { '__repo__' : repo.url }
696        for pName in projects:
697            p = {}
698            ans[pName] = p
699            # generate the definition of each target in the project
700            for tName, target in repo.projects[pName].targets.iteritems():
701                if not target.externals:
702                    p[tName] = {}
703                    continue
704
705                t = {'externals':{}}
706                p[tName] = t
707                for e in target.externals.itervalues():
708                    if e.target is None or \
709                           self.projects.get(e.target.project.name) is None:
710                        ref = Description_External(e.path, link=e.url)
711                    else:
712                        ref = Description_External \
713                            (e.path, project=e.target.project.name)
714                        ref.target = e.target.name
715                    if e.deep_ref:
716                        ref.deep_ref = e.deep_ref.strip('/')
717                        if len(ref.deep_ref) == 0:
718                            ref.deep_ref = None
719                    if e.rev:
720                        ref.rev = e.rev
721                    t['externals'][e.path] = ref
722
723            # attempt to reduce the definition to something closer to
724            # what a user would write
725            if 'trunk' in p:
726                trunk = p['trunk']
727                trunkExternals = trunk.get('externals', {})
728                for tName, target in p.iteritems():
729                    if tName == 'trunk':
730                        continue
731                    pegged = False
732                    for pegDir in self.config['pegged_targets']:
733                        if tName.startswith(pegDir):
734                            pegged = True
735                    if pegged:
736                        _reduce_pegged(target)
737                    else:
738                        _reduce_inherited(trunkExternals, target)
739
740        return ans
741
742    def collapse_repo_definition(self, rd):
743        """Collapse a repository definition dictionary down to a format
744        that generates more human-readable and editable YAML."""
745
746        for pName, p in rd.iteritems():
747            if pName == '__repo__':
748                continue
749            for tName, t in p.iteritems():
750                eDict = t.setdefault('externals', {})
751                for eName, e in eDict.iteritems():
752                    if isinstance(e, Description_External):
753                        eDict[eName] = str(e)
754                if not len(eDict):
755                    del t['externals']
756
757                eDict = t.setdefault('retarget', {})
758                for eName, e in eDict.iteritems():
759                    if isinstance(e, Description_Retarget):
760                        eDict[eName] = str(e)
761                if not len(eDict):
762                    del t['retarget']
763
764                inherit = t.setdefault('inherit', [])
765                for i in range(len(inherit)):
766                    if isinstance(inherit[i], Description_Inherit):
767                        inherit[i] = str(inherit[i])
768                if not len(inherit):
769                    del t['inherit']
770
771                branch = t.setdefault('branched-from', "")
772                if isinstance(branch, Description_Inherit):
773                    t['branched-from'] = branch = str(branch)
774                if not branch:
775                    del t['branched-from']
776
777                if 'finalized' in t:
778                    del t['finalized']
779                if 'source' in t:
780                    del t['source']
781        return rd
782
783    def expand_repo_definition(self, rd):
784        """Expand a repository definition dictionary that was just
785        loaded from YAML to the native form."""
786
787        repo = rd.get('__repo__', None)
788        if repo:
789            repo = self.get_repository(repo)
790        for pName, p in rd.iteritems():
791            if pName == '__repo__':
792                continue
793            for tName, t in p.items():
794                if not t:
795                    t = p[tName] = {}
796                    continue
797
798                eDict = t.get('externals') or {}
799                for eName, e in eDict.items():
800                    if isinstance(e, str):
801                        eDict[eName] = Description_External.parse_from(
802                            eName, e, self, repo )
803                t['externals'] = eDict
804
805                eDict = t.get('retarget') or {}
806                for eName, e in eDict.items():
807                    if isinstance(e, str):
808                        eDict[eName] = Description_Retarget.parse_from(eName, e)
809                t['retarget'] = eDict
810
811                inherit = t.get('inherit') or []
812                for i in range(len(inherit)):
813                    if inherit[i] and isinstance(inherit[i], str):
814                        inherit[i] = Description_Inherit.parse_from(inherit[i])
815                t['inherit'] = inherit
816
817                branch = t.get('branched-from') or None
818                if isinstance(branch, str):
819                    if branch:
820                        branch = Description_Inherit.parse_from(branch)
821                    else:
822                        branch = None
823                t['branched-from'] = branch
824
825        return rd
826
827    def finalize_repo_definition(self, rd):
828        def _finalize_external_list(projects, rd, ans):
829            project = projects.pop()
830            src = rd.get(project[0],{}).get(project[1], None)
831            if src is None:
832                # Target miss. Throw an exception?
833                log.warning("Target miss! (%s %% %s)" % project)
834                return
835            dest = ans.setdefault(project[0],{}).setdefault(project[1], {})
836            if dest.get('finalized', False):
837                # Already processed
838                return
839            # Check to make sure the project(s) we inherit from are processed
840            inherit = src.get('inherit', [])
841            base = []
842            for i in inherit:
843                b = ans.get(i.project or project[0],{}).get(i.target,{})
844                if not b.get('finalized', False):
845                    projects.append(project)
846                    projects.append((i.project or project[0], i.target))
847                    return
848                base.append(b)
849
850            branch = src.get('branched-from',None)
851            if len(inherit) and branch is not None:
852                raise Exception("Cannot have both 'inherit' and "
853                                "'branched-from' in target definition")
854
855            dest['inherit'] = inherit
856            if len(inherit):
857                dest['source'] = inherit
858            elif branch is not None:
859                dest['source'] = [ branch ]
860            else:
861                dest['source'] = []
862            dest['externals'] = {}
863            dest['retarget'] = {}
864            for b in base:
865                dest['externals'].update( deepcopy(b.get('externals', {})) )
866                dest['retarget'].update( deepcopy(b.get('retarget', {})) )
867            for remove in src.get('remove', []):
868                dest['externals'].pop(remove, 0)
869            for path, external in src.get('externals', {}).iteritems():
870                dest['externals'][path] = deepcopy(external)
871                if not external.target:
872                    dest['externals'][path].target = project[1]
873            for path, retarget in src.get('retarget', {}).iteritems():
874                dest['retarget'][path] = deepcopy(retarget)
875            dest['finalized'] = True
876        # END def _finalize_external_list
877
878        ans = {}
879        projects = []
880        for pName, p in rd.iteritems():
881            if pName == '__repo__':
882                ans[pName] = p
883                continue
884            projects.extend([(pName, tName) for tName in p.iterkeys()])
885        while projects:
886            _finalize_external_list(projects, rd, ans)
887
888        # Expand the retargeted externals
889        for pName, p in ans.iteritems():
890            if pName == '__repo__':
891                continue
892            for tName, t in p.iteritems():
893                retarget = t.get('retarget', {})
894                for eName, e in t.get('externals', {}).iteritems():
895                    if e.link:
896                        continue
897                    if e.project:
898                        rt = retarget.get(e.project, None)
899                        if rt is not None:
900                            e.target = rt.target
901                            e.rev = rt.rev
902                    if not e.target:
903                        e.target = tName
904        return ans
905
906
907    def generate_changeset_from_definition(self, defn):
908        repo = None
909        for r in self.all_repos:
910            if defn.get('__repo__') == r.url:
911                repo = r
912                break
913        if repo is None:
914            raise ExternalError("Cannot identify repository that "
915                                "matches definition")
916
917        changeset = { 'add' : [], 'del' : [], 'mod' : {}, 'repo' : repo }
918        pNames = sorted(set(defn.iterkeys()).union(repo.projects.keys()))
919        for pName in pNames:
920            if pName == '__repo__':
921                continue
922            p = repo.projects.get(pName, None)
923            dp = defn.get(pName, None)
924            if p is None:
925                # inserted
926                changeset['add'].append((pName, None))
927                continue
928            if dp is None:
929                # deleted
930                changeset['del'].append((p, None))
931                continue
932            # validate each target
933            tNames = sorted(set(dp.iterkeys()).union(p.targets.keys()))
934            for tName in tNames:
935                t = p.targets.get(tName, None)
936                dt = dp.get(tName, None)
937                if t is None:
938                    # inserted
939                    changeset['add'].append((p, tName))
940                    continue
941                if dt is None:
942                    # deleted
943                    changeset['del'].append((p, t))
944                    continue
945                # validate each external
946                tPrinted = False
947                eNames = sorted( set(t.externals.iterkeys()).union \
948                                 ( dt.get('externals',{}).keys() ) )
949                for eName in eNames:
950                    e = t.externals.get(eName)
951                    de = (dt.get('externals') or {}).get(eName)
952                    if e is None:
953                        # inserted
954                        changeset['mod'].setdefault(t, []).append((None, de))
955                    elif de is None:
956                        # deleted
957                        changeset['mod'].setdefault(t, []).append((e, None))
958                    elif e.url != de.url(self, repo) or e.rev != de.rev:
959                        # changed
960                        changeset['mod'].setdefault(t, []).append((e, de))
961        return changeset
962
963    def print_changeset(self, changeset):
964        repo = changeset['repo']
965        for target in changeset['add']:
966            if target[1] is None:
967                print("A %s" % target[0])
968            else:
969                print("A %s %% %s" % (target[0].name, target[1]))
970        for target in changeset['del']:
971            print("D %s" % ( target[1] and target[1] or target[0].name ))
972        for target in sorted( changeset['mod'].iteritems(),
973                              cmp=lambda x,y: cmp(str(x[0]), str(y[0])) ):
974            print("  %s" % target[0])
975            for e in target[1]:
976                if e[0] is not None:
977                    print("-     %s: %s @ %s" % (e[0].path, e[0].url, e[0].rev))
978                if e[1] is not None:
979                    print("+     %s: %s @ %s" % \
980                          (e[1].path, e[1].url(self, repo), e[1].rev))
981
982
983    def implement_changeset(self, changeset, defn):
984        if not VerifySvnVersion([1,5]):
985            raise SvnError( "implementing a changeset requires svn version "
986                            ">= 1.5, only found " + str(SvnVersion) )
987        if len(changeset['add']) + len(changeset['del']) + \
988               len(changeset['mod']) == 0:
989            return
990
991        allCheckouts = []
992        repo = changeset.get('repo')
993        if repo is None:
994            raise ExternalError("changeset had unresolved repository")
995
996        if os.path.exists( self.config['workingdir'] ):
997            if os.path.isdir( self.config['workingdir'] ):
998                log.warning( "working directory (%s) exists!" % \
999                             self.config['workingdir'] )
1000            else:
1001                raise SvnError( "working directory (%s) exists " \
1002                                "(and is not a directory)!" % \
1003                                 self.config['workingdir'] )
1004
1005        class externalFile(object):
1006            def __init__(self, dir):
1007                self.comments = []
1008                self.data = {}
1009                self.dir = dir
1010                self.name = 'Externals'
1011                self.svn15format = False
1012
1013            def fileName(self):
1014                return os.path.join(self.dir, self.name)
1015
1016        def _readExternalsFile(eInfo, target_wd):
1017            if isinstance(eInfo, str):
1018                e = eInfo
1019            else:
1020                e = eInfo[0]
1021                if e is None:
1022                    e = eInfo[1]
1023                e = e.path
1024            eDir, eName = os.path.split(e)
1025            ans = externalFile(os.path.join(target_wd, eDir))
1026            if not eName:
1027                raise ExternalError("external path ended with '/'")
1028            try:
1029                f = open(ans.fileName(), 'r')
1030                data1 = f.readlines()
1031                f.close()
1032                data2 = svn_propget('svn:externals', ans.dir)
1033                if data1 != data2:
1034                    log.warning("Externals file does not match "
1035                                "svn:externals property for (%s)!  "
1036                                "Deferring to the svn:externals property."
1037                                % ( ans.dir, ))
1038                data = data2
1039            except:
1040                try:
1041                    data = svn_propget('svn:externals', ans.dir)
1042                except Exception:
1043                    data = []
1044
1045            # 2 passes: first guess the file format, then parse the file
1046            for line in data:
1047                line = line.strip()
1048                if line.startswith('#') or len(line) == 0:
1049                    continue
1050                if externals_14link_re.match(line):
1051                    continue
1052                if externals_15link_re.match(line):
1053                    ans.svn15format = True
1054                    break
1055
1056            lNum = 0
1057            for line in data:
1058                line = line.strip()
1059                if line.startswith('#') or len(line) == 0:
1060                    ans.comments.append( (lNum, line+'\n') )
1061                    continue
1062                g = externals_14link_re.match(line)
1063                if g and not ans.svn15format:
1064                    lNum += 1
1065                    ans.data[g.group(1)] = [ [ g.group(3), g.group(2) ],
1066                                             lNum ]
1067                    continue
1068                g = externals_15link_re.match(line)
1069                if g:
1070                    lNum += 1
1071                    ans.svn15format = True
1072                    ans.data[g.group(4)] = [ [ g.group(2),
1073                                               g.group(3) or g.group(1) ],
1074                                             lNum ]
1075                    continue
1076               
1077            # Suppress trailing blank lines
1078            while len(ans.comments) and ans.comments[-1][0] == lNum and \
1079                      not ans.comments[-1][1].strip():
1080                ans.comments.pop()
1081
1082            return (ans, eName);
1083        # end _readExternalsFile()
1084
1085        def _writeExternalsFile(eFile):
1086            eList = sorted( eFile.data.keys(),
1087                            cmp=lambda x,y: cmp(x.lstrip('#'), y.lstrip('#')) )
1088            srcW = 0;
1089            targetW = 0;
1090            revW = 0;
1091            for e in eList:
1092                srcW = max(srcW, len(str(eFile.data[e][0][1])))
1093                targetW = max(targetW, len(e))
1094                if len(eFile.data[e][0]) > 1 and eFile.data[e][0][1] is not None:
1095                    revW = max(revW, len(str(eFile.data[e][0][1]))+3)
1096            if eFile.svn15format:
1097                field = "%%-%is%%-%is %%s\n" % ( revW, srcW )
1098            else:
1099                field = "%%-%is %%-%is%%s\n" % ( targetW, revW )
1100
1101            lNum = 0;
1102            f = open(eFile.fileName(), 'w')
1103            while len(eList) > 0:
1104                while len(eFile.comments) and eFile.comments[0][0] <= lNum:
1105                    f.write(eFile.comments.pop(0)[1])
1106
1107                e = eList.pop(0)
1108                uncommentedE = e.lstrip('#')
1109                if uncommentedE != e:
1110                    if uncommentedE in eFile.data.keys():
1111                        continue
1112                if len(eFile.data[e][0]) == 1 or eFile.data[e][0][1] is None:
1113                    tmp = (e, '', eFile.data[e][0][0]);
1114                else:
1115                    tmp = (e, '-r'+str(eFile.data[e][0][1]), eFile.data[e][0][0]);
1116                if eFile.svn15format:
1117                    tmp = (tmp[1], tmp[2], tmp[0])
1118                f.write( field % tmp )
1119                lNum = eFile.data[e][1]
1120            while len(eFile.comments) and not eFile.comments[-1][1].strip():
1121                eFile.comments.pop()
1122            while len(eFile.comments):
1123                f.write(eFile.comments.pop(0)[1])
1124            f.close()
1125            p = Popen( [ 'svn', 'propset', 'svn:externals', eFile.dir,
1126                         '-F', eFile.fileName() ],  stdout=PIPE, stderr=PIPE )
1127            stdout, stderr = p.communicate()
1128            if p.returncode:
1129                raise SvnError( "svn propset returned error %i:\n%s" %
1130                        (p.returncode, stderr) )
1131        # end _writeExternalsFile()
1132
1133        def _checkoutTarget(item):
1134            if isinstance(item, ResolvedTarget):
1135                wd = os.path.join(self.config['workingdir'], item.project.name, item.name)
1136                url = item.url
1137            elif isinstance(item, ResolvedProject):
1138                wd = os.path.join(self.config['workingdir'], item.name)
1139                url = item.url
1140            else:
1141                log.warning("I cannot checkout a new project without " \
1142                            "checking out the WHOLE repository!")
1143                return
1144
1145            if not url.startswith(repo.url):
1146                raise ExternalError("Target URL does not begin with Repo URL")
1147
1148            wd = self.config['workingdir']
1149            url = repo.url
1150            for d in item.url[len(repo.url):].replace('\\','/').strip('/').split('/'):
1151                if len(d) == 0:
1152                    continue
1153                if not os.path.isdir(wd):
1154                    log.info("checking out " + url + " to " + wd + \
1155                             " (as empty directory)")
1156                    p = Popen( [ 'svn', 'co', '--depth=empty', url, wd ],
1157                               stdout=PIPE, stderr=PIPE )
1158                    stdout, stderr = p.communicate()
1159                    if p.returncode:
1160                        raise SvnError( "svn checkout returned error %i:\n%s" %
1161                                        (p.returncode, stderr) )
1162                wd = os.path.join(wd, d)
1163                url += '/' + d
1164
1165            if os.path.isdir(wd):
1166                return wd
1167            log.info("checking out " + url)
1168            p = Popen( ['svn', 'co', '--ignore-externals', url, wd ],
1169                       stdout=PIPE, stderr=PIPE )
1170            stdout, stderr = p.communicate()
1171            if p.returncode:
1172                raise SvnError( "svn checkout returned error %i:\n%s" %
1173                        (p.returncode, stderr) )
1174            allCheckouts.append(wd)
1175            return wd
1176        # end _checkoutTarget()
1177
1178        # First checkout anything where we need the whole project
1179        # (i.e. add / del)
1180        for target in changeset['del']:
1181            if target[1] is None:
1182                # Cannot automatically delete project
1183                continue
1184            _checkoutTarget(target[0])
1185        for target in changeset['add']:
1186            if target[1] is None:
1187                # Cannot automatically add new project
1188                continue
1189            _checkoutTarget(target[0])
1190
1191        # Update any *changed* targets
1192        for t, eList in changeset['mod'].iteritems():
1193            log.info("Updating %s" % t)
1194            target_wd = _checkoutTarget(t)
1195            for e in eList:
1196                (eFile, eName) = _readExternalsFile(e, target_wd)
1197
1198                if e[0] is None:
1199                    eFile.data[eName] = [[e[1].url(self, repo), e[1].rev], -1]
1200                elif e[1] is None and eName in eFile.data:
1201                    del eFile.data[eName]
1202                else:
1203                    eFile.data.setdefault(eName, [None, -1])[0] = \
1204                        [ e[1].url(self, repo), e[1].rev ]
1205
1206                _writeExternalsFile(eFile)
1207
1208        # Add any new targets
1209        for target in changeset['add']:
1210            if target[1] is None:
1211                log.warning("Cannot automatically add project '%s'" % \
1212                            target[0])
1213                continue
1214            target_def = defn[target[0].name].get(target[1], None)
1215            if target_def is None:
1216                log.error("Cannot create target '%s %% %s': no definition" % \
1217                          (target[0].name, target[1]))
1218                continue
1219            wd = os.path.join(
1220                self.config['workingdir'], target[0].name, target[1] )
1221            if not os.path.isdir(wd):
1222                inherit = target_def.get('source', [])
1223                if len(inherit) == 0:
1224                    log.error("Cannot create target '%s %% %s': %s" % (
1225                        target[0].name, target[1], "nothing to inherit from" ))
1226                    continue
1227                elif len(inherit) != 1:
1228                    log.error("Cannot create target '%s %% %s': %s" %
1229                              ( target[0].name, target[1],
1230                                "no support for multiple inheritance" ))
1231                    continue
1232
1233                src = repo.projects.get(inherit[0].project or target[0].name)
1234                if src is not None:
1235                    src = src.targets.get(inherit[0].target)
1236                if src is None:
1237                    log.error("Cannot create target '%s %% %s': %s" % \
1238                              ( target[0].name, target[1],
1239                                "base target is not in the repository?" ))
1240                    continue
1241                base = _checkoutTarget(src)
1242
1243                log.info("Copying %s from %s" % ( target[1], base ))
1244                p = Popen( ['svn', 'cp', '--parents', base, wd],
1245                           stdout=PIPE, stderr=PIPE )
1246                stdout, stderr = p.communicate()
1247                if p.returncode:
1248                    raise SvnError( "svn cp returned error %i:\n%s" %
1249                                    (p.returncode, stderr) )
1250            log.info("Attempting to initialize externals for %s" % target[1])
1251            current_externals, svn15format = svn_externals(wd)
1252            print(current_externals)
1253            for e in current_externals:
1254                (eFile, eName) = _readExternalsFile(e.path, wd)
1255                try:
1256                    del eFile.data[eName]
1257                except KeyError:
1258                    pass
1259                _writeExternalsFile(eFile)
1260            for name, e in target_def.get('externals',{}).iteritems():
1261                (eFile, eName) = _readExternalsFile(name, wd)
1262                eFile.data[eName] = [ [ e.url(self, repo), e.rev ], -1 ]
1263                _writeExternalsFile(eFile)
1264
1265
1266        # Delete any old targets
1267        for target in changeset['del']:
1268            if target[1] is None:
1269                log.warning("Cannot automatically delete project '%s'" % \
1270                            target[0].name)
1271                continue
1272            log.info("Deleting %s" % target[1])
1273            target_wd = _checkoutTarget(target[1])
1274            p = Popen( ['svn', 'rm', target_wd],  stdout=PIPE, stderr=PIPE )
1275            stdout, stderr = p.communicate()
1276            if p.returncode:
1277                raise SvnError( "svn rm returned error %i:\n%s" %
1278                        (p.returncode, stderr) )
1279
1280        # List changed locations
1281        log.info("The following directories need to be committed:\n" + \
1282                 ' '.join(allCheckouts))
1283
1284    def branch_config(self, cfg):
1285        def _get_target(em, cfg, external):
1286            if type(external) is ResolvedProject:
1287                proj = external
1288                target = None
1289                current = ""
1290                msg = "Branch project from"
1291            else:
1292                target = external.target
1293                if target is None:
1294                    raise Exception( "External %s pointing to an unknown "
1295                                     "target, %s" %
1296                                     (external.path, external.url) )
1297                proj = target.project
1298                current = "current: %s, " % (external.target.name, )
1299                if proj in cfg['retargeted']:
1300                    target = cfg['retargeted'][proj]
1301                else:
1302                    target = external.target
1303                msg = "Target for external \"%s : %s\"" % (
1304                    external.path, proj.name )
1305                       
1306            i = -1;
1307            for t in proj.targets.itervalues():
1308                if t.rev > i:
1309                    i = t.rev
1310                    newest = t
1311            if target is None:
1312                target = newest
1313            newer = target is not newest and "*" or ""
1314            msg = "%s [%s%s] (%s? for list): " % \
1315                  (msg, target.name, newer, current)
1316            while True:
1317                sys.stdout.write(msg)
1318                ans = sys.stdin.readline().strip()
1319                if ans == "":
1320                    return target
1321                elif ans in proj.targets:
1322                    return proj.targets[ans]
1323                elif ans.isdigit():
1324                    num = int(ans)-1
1325                    if num >= 0 and num < len(proj.targets):
1326                        return proj.targets[sorted(proj.targets.keys())[num]]
1327                    log.error("Invalid target id)")
1328                elif ans == "?":
1329                    for idx, name in enumerate(sorted(proj.targets.keys())):
1330                        star = proj.targets[name] is newest and "*" or ""
1331                        print("   %2d: %s%s" % ( idx+1, name, star ))
1332                else:
1333                    log.error("(Unrecognized target)")
1334
1335        cfg.setdefault('retargeted', {})
1336        if cfg.get('project', None) is None:
1337            sys.stdout.write("Branch project name: ")
1338            cfg['project'] = sys.stdin.readline().strip()
1339        proj = cfg['project']
1340        if type(proj) is not ResolvedProject:
1341            cfg['project'] = proj = self.get_project(proj)
1342            if proj is None:
1343                raise ExternalError("branch_config(): bad project (%s)",
1344                                    (cfg['project'],) )
1345       
1346        if cfg.get('src', None) is None:
1347            cfg['src'] = _get_target(self, cfg, proj)
1348        elif type(cfg['src']) is str:
1349            cfg['src'] = proj.targets.get(cfg['src'], None)
1350        src = cfg['src']
1351        if type(src) is not ResolvedTarget:
1352            raise ExternalError("branch_config(): bad target (%s)", (src,))
1353       
1354        if cfg.get('target', None) is None:
1355            sys.stdout.write("Enter new target name: ")
1356            cfg['target'] = sys.stdin.readline().strip()
1357        if cfg['target'] in proj.targets:
1358            raise ExternalError("Cannot create new branch, %s: target "
1359                                "already exists" % ( cfg['target'], ))
1360        for c in illegal_characters:
1361            if c in cfg['target']:
1362                raise ExternalError("Illegal character (\"%s\") found in "
1363                                    "target name (\"%s\"", (c, cfg['target']))
1364
1365        e_map = cfg.setdefault('externals',{})
1366        for e_path in sorted(src.externals.keys()):
1367            if e_path in e_map:
1368                continue
1369            t = _get_target(self, cfg, src.externals[e_path])
1370            if cfg['retargeted'].setdefault(t.project, t) is not t:
1371                orig = cfg['retargeted'][t.project]
1372                log.warning( "Externals pointing to multiple "
1373                             "targets (%s, %s) in project %s " %
1374                             ( orig is None and "*" or orig.name,
1375                               t.name, t.project.name ) )
1376                cfg['retargeted'][t.project] = None
1377            e_map[e_path] = t
1378
1379        ans = { '__repo__' : proj.repo.url }
1380        defn = ans.setdefault(proj.name,{}).setdefault(cfg['target'], {})
1381        if cfg.get('peg', False):
1382            defn['branched-from'] = Description_Inherit(src.name)
1383            externals = defn.setdefault('externals', {})
1384            for e_path, old_e in src.externals.iteritems():
1385                target = e_map[e_path]
1386                new_e = externals[e_path] = Description_External(
1387                    e_path, project=target.project.name )
1388                new_e.target = target.name
1389                new_e.deep_ref = old_e.deep_ref
1390                if target is old_e.target and old_e.rev:
1391                    new_e.rev = old_e.rev
1392                else:
1393                    new_e.rev = target.project.repo.rev
1394                # Peg to a URL (and not a project / target reference)
1395                new_e.link = new_e.url(self, proj.repo)
1396                new_e.project = None
1397        else:
1398            defn['inherit'] = [ Description_Inherit(src.name) ]
1399            externals = defn.setdefault('externals', {})
1400            retarget = defn.setdefault('retarget', {})
1401            remove = defn.setdefault('remove', [])
1402            for e_path in sorted(src.externals):
1403                old_e = src.externals[e_path]
1404                target = e_map[e_path]
1405                retargeted = cfg['retargeted'].get(target.project, None)
1406                if target is retargeted:
1407                    if target is old_e.target:
1408                        continue
1409                    retarget[target.project.name] = Description_Retarget(
1410                        retargeted.name )
1411                else:
1412                    if retargeted is None and target is old_e.target:
1413                        continue
1414                    remove.append(e_path)
1415                    new_e = externals[e_path] = Description_External(
1416                        e_path, project=target.project.name )
1417                    new_e.target = target.name
1418                    new_e.deep_ref = old_e.deep_ref
1419            if len(externals) == 0:
1420                del defn['externals']
1421            if len(retarget) == 0:
1422                del defn['retarget']
1423            if len(remove) == 0:
1424                del defn['remove']
1425        return ans, proj.name
1426
1427    def _resolve_external(self, e):
1428        # Did we resolve the external target?
1429        if e.target is not None:
1430            # But warn if the target was resolved through an alias
1431            if e.url != e.raw.url:
1432                return Status.aliased_url, e.target.rev
1433            return Status.ok, e.target.rev
1434        # Is the target outside our defined "scope"?
1435        url = e.url
1436        for ex in self.config['exclude']:
1437            if url.startswith(ex):
1438                return Status.excluded, None
1439
1440        # Is the target a currently valid repo location?
1441        try:
1442            info = svn_info(url)
1443            if 'revision' in info:
1444                return Status.non_target, info['revision']
1445        except SvnError:
1446            pass
1447        # OK, we may have a problem... if the external pegged a
1448        # revision, did the target exist in that revision?
1449        if e.rev is not None:
1450            try:
1451                info = svn_info(url, e.rev)
1452                if 'revision' in info:
1453                    return Status.dne_in_head, info['revision']
1454            except SvnError:
1455                pass
1456        # This is definitely a broken external
1457        return Status.broken, None
1458
1459
1460if __name__ == "__main__":
1461    # Check for sane usage.
1462    em = ExternalManager()
1463
1464    """
1465    # you can grow old and grey waiting for COIN-OR
1466    em.config['exclude'].append('https://projects.coin-or.org/')
1467
1468    # Special cases:
1469    #   - EXACT is a pointer to FAST
1470    em.config['aliases'].append(( 'https://software.sandia.gov/svn/public/exact',
1471                        'https://software.sandia.gov/svn/public/fast' ))
1472    #   - software/svn/public is available via https
1473    em.config['aliases'].append(( 'svn+ssh://software.sandia.gov/svn/public',
1474                        'https://software.sandia.gov/svn/public' ))
1475    #   - DAKOTA used malformed URLs
1476    em.config['aliases'].append(( 'https://software.sandia.gov:/',
1477                        'https://software.sandia.gov/' ))
1478
1479    # Unfortunately, the CxxTest repo and our mirror use the same name
1480    em.config['primary_projects']['cxxtest'] =  \
1481        'https://software.sandia.gov/svn/public/cxxtest:'
1482    em.config['primary_projects']['boost'] =  \
1483        'https://software.sandia.gov/svn/public/tpl:boost'
1484
1485    print(yaml.dump( em.config, default_flow_style=False,
1486                     explicit_start=True, explicit_end=True, width=160 ) \
1487                     .replace('{}',''))
1488    """
1489    # The previous config translates to the following config.yaml:
1490    """
1491    ---
1492    aliases:
1493    - !!python/tuple
1494      - https://software.sandia.gov/svn/public/exact
1495      - https://software.sandia.gov/svn/public/fast
1496    - !!python/tuple
1497      - svn+ssh://software.sandia.gov/svn/public
1498      - https://software.sandia.gov/svn/public
1499    - !!python/tuple
1500      - https://software.sandia.gov:/
1501      - https://software.sandia.gov/
1502    exclude:
1503      - https://projects.coin-or.org/
1504    pegged_targets:
1505      - releases
1506      - tags
1507      - maintenance
1508    primary_projects:
1509      boost: https://software.sandia.gov/svn/public/tpl:boost
1510      cxxtest: 'https://software.sandia.gov/svn/public/cxxtest:'
1511    workingdir: svnpm-tmp
1512    ...
1513    """
1514
1515    #em.update()
1516    em.resolve_targets()
1517    #em.check_integrity()
1518    #em.check_pegged()
1519    #em.check_pegged(repo=em.projects['autodock-th'].repo)
1520
1521    #ref_map = em.reference_map()
1522    #for target in sorted(ref_map.keys(), key=lambda t : str(t)):
1523    #    print(str(target))
1524    #    for r in ref_map[target]:
1525    #        print("   %s" % str(r))
1526
1527    #descrip = em.generate_repo_definition(proj="acro-utilib")
1528    #descrip = em.generate_repo_definition(repo=em.projects["acro-utilib"].repo)
1529    #final = em.finalize_repo_definition(descrip)
1530    #em.collapse_repo_definition(descrip)
1531    #em.collapse_repo_definition(final)
1532    #print(yaml.dump( descrip, default_flow_style=False,
1533    #                 explicit_start=True, explicit_end=True, width=160 ) \
1534    #                 .replace('{}',''))
1535
1536    FILE = open('acro.yaml')
1537    user_descrip = yaml.load(FILE)
1538    em.expand_repo_definition(user_descrip)
1539    FILE.close()
1540    descrip = em.finalize_repo_definition(user_descrip)
1541    changes = em.generate_changeset_from_definition(descrip)
1542    em.print_changeset(changes)
1543    #em.implement_changeset(changes, descrip, em.projects["acro"].repo)
Note: See TracBrowser for help on using the repository browser.