source: CMT/HEAD/mgr/cmt_svn_checkout.py @ 605

Last change on this file since 605 was 605, checked in by rybkin, 12 years ago

See C.L. 480

  • Property svn:executable set to *
File size: 22.6 KB
Line 
1#!/usr/bin/env python
2
3"""cmt_svn_checkout.py: Checkout CMT package(s) from Subversion repository.
4
5Usage: cmt_svn_checkout.py [OPTION]... PATH...
6Checkout PATH(s) relative to or being Subversion repository URL(s).
7
8Mandatory arguments to long options are mandatory for short options too.
9  -r, --version-tag=REV        checkout version tag REV of PATH
10      --version-dir=DIR        use DIR as version directory instead of version tag
11  -d, --directory=DIR          checkout into DIR instead of (basename of) PATH
12  -o, --offset=OFFSET          checkout PATH at OFFSET relative to repository URL
13      --no_config              disable config step upon checkout
14      --without_version_directory do not create version directory upon PATH checkout
15      --with_version_directory create version directory upon PATH checkout (default)
16      --url=URL                checkout PATH from repository URL
17  -h, --help                   display this help and exit
18      --version                output version information and exit
19
20The SVNROOT, SVNTRUNK, SVNTAGS, and SVNBRANCHES environment variables specify repository URL of PATH, location of PATH trunk, tags, and branches (relatively to PATH) respectively.
21
22Report bugs to <CMT-L@IN2P3.FR>.
23"""
24
25__version__ = '0.6.0'
26__date__ = 'Fri Mar 09 2012'
27__author__ = 'Grigory Rybkin'
28
29import sys
30import getopt
31import os
32import posixpath
33import os.path
34import urlparse
35# for Python 2.3 and older
36if sys.version_info[0] == 2 and sys.version_info[1] < 4:
37    for p in ('svn', 'svn+ssh'):
38        if p not in urlparse.uses_netloc:
39            urlparse.uses_netloc.append(p)
40import tempfile
41import re
42
43self = 'cmt_svn_checkout.py'
44# try:
45#     from svn import client, core
46# except ImportError, e:
47#     print >>sys.stderr, '%s: cannot import Subversion Python bindings: %s' \
48#           % (self, str(e))
49#     sys.exit(1)
50os.environ['LC_ALL'] = 'C'
51
52class Utils(object):
53    def getstatusoutput(cmd):
54        """Return (status, stdout + stderr) of executing cmd in a shell.
55       
56        A trailing line separator is removed from the output string. The exit status of the command is encoded in the format specified for wait(), when the exit status is zero (termination without errors), 0 is returned.
57        """
58        p = os.popen('( %s ) 2>&1' % cmd, 'r')
59        out = p.read()
60        sts = p.close()
61        if sts is None: sts = 0
62        if out.endswith(os.linesep):
63            out = out[:out.rindex(os.linesep)]
64        elif out[-1:] == '\n': out = out[:-1]
65        return sts, out
66    getstatusoutput = staticmethod(getstatusoutput)
67   
68    def getstatuserror(cmd):
69        """Return (status, stderr) of executing cmd in a shell.
70       
71        On Unix, the return value is the exit status of the command is encoded in the format specified for wait(). On Windows, on command.com systems (Windows 95, 98 and ME) this is always 0; on cmd.exe systems (Windows NT, 2000 and XP) this is the exit status of the command run.
72        """
73        fd, p = tempfile.mkstemp()
74        os.close(fd)
75#        print >> sys.stderr, 'Created file %s with fd %i' % (p, fd)
76#        p = os.tempnam()
77#        print >> sys.stderr, 'Created file name %s' % (p)
78        sc = os.system('( %s ) 2>%s' % (cmd, p))
79        f = open(p)
80        e = f.read()
81        f.close()
82        os.unlink(p)
83        return sc, e
84    getstatuserror = staticmethod(getstatuserror)
85
86class ClientContext(object):
87
88    schemes = ('http', 'https', 'svn', 'svn+ssh', 'file')
89
90    def svn_path_canonicalize(self, path):
91        """Return a new path (or URL) like path, but transformed such that some types of path specification redundancies are removed.
92
93        This involves collapsing redundant "/./" elements, removing multiple adjacent separator characters, removing trailing separator characters, and possibly other semantically inoperative transformations. Convert the scheme and hostname to lowercase."""
94        scheme, netloc, path, query, fragment = urlparse.urlsplit(path)
95        scheme = scheme.lower()
96        netloc = netloc.lower()
97        if path.startswith('/'): b = '/'
98        else: b = ''
99        path = b + '/'.join([s for s in path.split('/') if s and s != '.'])
100        return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
101
102    def urljoin(self, *args):
103        urls = [urlparse.urlsplit(arg) for arg in args]
104        if not urls: return ''
105       
106        schemes = [url[0] for url in urls]
107        schemes.reverse()
108        for i, s in enumerate(schemes):
109            if s and s in self.schemes:
110                scheme = s
111                index = i
112                break
113        else:
114            scheme = ''
115            index = len(urls) - 1
116       
117        netlocs = [url[1] for url in urls]
118        netlocs.reverse()
119        for i, s in enumerate(netlocs[:index + 1]):
120            if s:
121                netloc = s
122                index = i
123                break
124        else:
125            netloc = ''
126       
127        path = posixpath.join(*[url[2] for url in urls][len(urls) - 1 - index:])
128
129        query = fragment = ''
130        return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
131
132#         scheme = self.last_nonempty([url[0] for url in urls if url[0] in self.schemes])
133#         netloc = self.last_nonempty([url[1] for url in urls])
134#         path = posixpath.join(*[url[2] for url in urls])
135#         query = self.last_nonempty([url[3] for url in urls])
136#         fragment = self.last_nonempty([url[4] for url in urls])
137       
138#         return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
139
140    def last_nonempty(self, list, default = ''):
141        list.reverse()
142        for i in list:
143            if i: return i
144        else:
145            return default
146       
147#     def __init__(self):
148#         core.svn_config_ensure(None)
149#         self.ctx = client.create_context()
150#         self.providers = [
151#             client.get_simple_provider(),
152#             client.get_username_provider(),
153#             client.get_ssl_server_trust_file_provider(),
154#             client.get_ssl_client_cert_file_provider(),
155#             client.get_ssl_client_cert_pw_file_provider()
156#             ]
157#         self.ctx.auth_baton = core.svn_auth_open(self.providers)
158#         self.ctx.config = core.svn_config_get_config(None)
159
160#     def __call__(self):
161#         return self.ctx
162
163def cwd():
164    _gcwd = os.getcwd()
165    try:
166        _cwd = os.environ['PWD']
167        if _cwd and os.path.samefile(_cwd, _gcwd):
168            return _cwd
169        else:
170            return _gcwd
171    except (KeyError, AttributeError):
172        return _gcwd
173
174def cd(path):
175    new = os.path.normpath(os.path.join(cwd(), path))
176    os.chdir(path)
177    _gcwd = os.getcwd()
178    try:
179        if os.path.samefile(new, _gcwd):
180            os.environ['PWD'] = new
181        else:
182            os.environ['PWD'] = _gcwd
183    except AttributeError:
184        pass
185
186def error(instance, location='', file = sys.stderr):
187    try:
188        message = ': '.join([str(arg) for arg in instance.args])
189    except AttributeError:
190        message = str(instance).rstrip()
191    if location: location += ': '
192    print >> file, "%s%s" % (location, message)
193   
194class CmtContext(object):
195    def __init__(self,
196                 config = True,
197                 with_version_directory = True,
198                 cleanup = False,
199                 head_version = None):
200        self.config = config
201        self.with_version_directory = with_version_directory
202        self.cleanup = cleanup
203        self.head_version = head_version
204        self.env()
205
206    def env(self):
207        if self.head_version is None:
208            self.set_head_version(os.getenv('CMTHEADVERSION', 'HEAD'))
209#        print >>sys.stderr, 'env: set head_version: %s' % self.head_version
210
211    def set_head_version(self, version):
212        self.head_version = str(version).replace('<package>', '%(package)s').replace('<PACKAGE>', '%(PACKAGE)s').replace('<revision>', '%(revision)i')
213
214    def eval_head_version(self, m):
215        return self.head_version % \
216               {'package' : m.package,
217                'PACKAGE' : m.package.upper(),
218                'revision' : m.info.last_changed_rev}
219
220    def write(self, p, version):
221        #print >> sys.stderr, 'write:', p, version
222        try:
223            t = version + '\n'
224            if os.path.exists(p):
225                f = open(p, 'r+')
226                b = f.tell()
227                v = f.read()
228                if v != t:
229                    f.seek(b)
230                    f.write(t)
231                    f.truncate()
232            else:
233                f = open(p, 'w')
234                f.write(t)
235            f.close()
236        except IOError, e:
237            print >> sys.stderr, e
238            return 1
239        return 0
240       
241    def generate(self, p):
242        #print >> sys.stderr, 'generate:', p
243        curdir = cwd()
244#        cmd = 'cmt -disable_warnings'
245        cmd = 'cmt -q'
246        if self.with_version_directory:
247            cmd += ' -with_version_directory'
248        else:
249            cmd += ' -without_version_directory'
250        if self.cleanup:
251            cmd += ' -cleanup'
252        else:
253            cmd += ' -no_cleanup'
254        cmd += ' config'
255        cd(p)
256        sc = os.system(cmd)
257        if sc != 0: sc = 1
258        cd(curdir)
259#        return sc
260        return 0
261
262    def configure(self, path, version):
263        sc = 0
264        for d in ('cmt', 'mgr'):
265            p = os.path.join(path, d)
266            if os.path.isdir(p):
267                if not self.with_version_directory:
268                    sc += self.write(os.path.join(p,'version.cmt'), version)
269                elif os.path.exists(os.path.join(p,'version.cmt')):
270                    print >>sys.stderr, 'Warning: file %s normally should not be under version control. Please, consider removing' % os.path.join(d,'version.cmt')
271                    try:
272                        os.rename(os.path.join(p,'version.cmt'),
273                                  os.path.join(p,'version.cmt.orig'))
274                        print >>sys.stderr, 'renamed %s -> %s' % (`os.path.join(p,'version.cmt')`, `os.path.join(p,'version.cmt.orig')`)
275                    except (IOError, os.error), e:
276                        error(e)
277                   
278                if self.config:
279                    sc += self.generate(p)
280                return sc
281        return sc
282        #print >> sys.stderr, 'Cannot configure %s ' % (path, version)
283
284class Module(object):
285    def __init__(self, module):
286        self.module = module
287        self.init = False
288        class info(object):
289            last_changed_rev = 0
290        self.info = info
291#         print >>sys.stderr, 'init module %s: last_changed_rev %i' % \
292#               (self.module, self.info.last_changed_rev)
293       
294class Checkout(object):
295    def __init__(self,
296                 url = None,
297                 trunk = None,
298                 tags = None,
299                 branches = None,
300                 version = None,
301                 version_dir = None,
302                 directory = None,
303                 offset = '',
304                 modules = []):
305        self.url = url
306        self.trunk = trunk
307        self.tags = tags
308        self.branches = branches
309        self.version = version
310        self.version_dir = version_dir
311        self.directory = directory
312        self.offset = offset
313        self.modules = modules
314        self.reposLayout()
315       
316    def reposLayout(self):
317        if self.url is None:
318            self.url = os.getenv('SVNROOT', '')
319#             try:
320#                 self.url = os.environ['SVNROOT']
321#             except KeyError:
322#                 pass
323        if self.trunk is None:
324            self.trunk = os.getenv('SVNTRUNK', 'trunk')
325        if self.tags is None:
326            self.tags = os.getenv('SVNTAGS', 'tags')
327        if self.branches is None:
328            self.branches = os.getenv('SVNBRANCHES', 'branches')
329
330    def cmtRepos(self):
331        self.url = 'https://svn.lal.in2p3.fr/projects/CMT'
332        self.trunk = 'HEAD'
333        self.tags= '.'
334        self.branches = '.'
335
336    def add(self, module):
337        self.modules.append(module)
338
339    def info_receiver(self, path, info, pool):
340        self.path = path
341        self.info = info
342        pool.info = info
343
344    def cmp(self, path1, path2, client_context):
345        cmd = 'svn diff %s %s' % (path1, path2)
346#        cmd = 'svn diff --summarize %s %s' % (path1, path2)
347        sc, out = Utils.getstatusoutput(cmd)
348        if sc != 0:
349            return 2
350        if out: return 1
351        else: return 0
352
353#         outfile = tempfile.TemporaryFile('wb')
354#         errfile = tempfile.TemporaryFile('wb')
355#         outb = outfile.tell()
356#         errb = errfile.tell()
357#         client.diff([]
358#                     , path1
359#                     , self.head_revision
360#                     , path2
361#                     , self.head_revision
362#                     , True, True, False
363#                     , outfile
364#                     , errfile
365#                     , client_context())
366
367#         # position at the end of the file
368#         outfile.seek(0, 2)
369#         errfile.seek(0, 2)
370#         oute = outfile.tell()
371#         erre = errfile.tell()
372
373#         if erre > errb: return 2
374#         elif oute > outb: return 1
375#         else: return 0
376
377    def trunk_tag(self, module, client_context):
378        """Attempt to determine the tag of the module's trunk.
379       
380        Return the tag if its contents are the same as those of the trunk, and
381        its last_changed_rev is greater than the trunk created_rev, None otherwise.
382        """
383        trunk = posixpath.join(module.url, self.trunk)
384#        trunk = posixpath.normpath(posixpath.join(module.url, self.trunk))
385        cmd = 'svn info %s' % trunk
386        sc, out = Utils.getstatusoutput(cmd)
387        if sc != 0:
388            return None
389        p = r'last\s+changed\s+rev:\s+(?P<rev>\d+)'
390        m = re.search(p, out, re.I)
391        if m:
392            class info(object): pass
393            info.last_changed_rev = int(m.group('rev'))
394            self.info_receiver(trunk, info, module)
395#            self.info_receiver(trunk, info, None)
396#             print >>sys.stderr, '%s: last_changed_rev %i' % \
397#                   (trunk, self.info.last_changed_rev)
398#            last_changed_rev = int(m.group('rev'))
399        else:
400            return None
401
402        tags = posixpath.join(module.url, self.tags)
403#        tags = posixpath.normpath(posixpath.join(module.url, self.tags))
404        cmd = 'svn ls -v %s' % tags
405        sc, out = Utils.getstatusoutput(cmd)
406        if sc != 0:
407            return None
408
409        tags_dirent = [line.split() for line in out.splitlines()]
410        rev_tag = dict([(int(line[0]), line[-1].rstrip(posixpath.sep)) for line in tags_dirent if line[-1].endswith(posixpath.sep)])
411        revs = rev_tag.keys()
412        revs.sort()
413        revs.reverse()
414
415        for rev in revs:
416            if rev < self.info.last_changed_rev: break
417#            if rev < last_changed_rev: break
418            tag = posixpath.join(tags, rev_tag[rev])
419#            tag = posixpath.normpath(posixpath.join(tags, rev_tag[rev]))
420            if 0 == self.cmp(trunk, tag, client_context):
421                return rev_tag[rev]
422
423        return None
424
425#         try:
426#             trunk = core.svn_path_canonicalize(posixpath.join(module.url, self.trunk))
427#             client.info(trunk,
428#                         self.head_revision,
429#                         self.head_revision,
430#                         self.info_receiver,
431#                         False,
432#                         client_context())
433           
434#             tags = core.svn_path_canonicalize(posixpath.join(module.url, self.tags))
435#             tags_dirent = client.ls(tags,
436#                                     self.head_revision,
437#                                     False,
438#                                     client_context())
439           
440#             rev_tag = dict([(tags_dirent[p].created_rev, p) for p in tags_dirent if tags_dirent[p].kind == core.svn_node_dir])
441#             revs = rev_tag.keys()
442#             revs.sort()
443#             revs.reverse()
444
445#             for rev in revs:
446#                 if rev < self.info.last_changed_rev: break
447#                 tag = core.svn_path_canonicalize(posixpath.join(tags, rev_tag[rev]))
448#                 if 0 == self.cmp(trunk, tag, client_context):
449#                     return rev_tag[rev]
450
451#             return None
452#         except core.SubversionException, e:
453#             return None
454
455    def initialize(self, cmt_context, client_context):
456        sc = 0
457#         self.head_revision = core.svn_opt_revision_t()
458#         self.head_revision.kind = core.svn_opt_revision_head
459
460        # canonicalize:
461        self.url = client_context.svn_path_canonicalize(self.url)
462        self.offset = client_context.svn_path_canonicalize(self.offset)
463
464        for m in self.modules:
465            m.module = client_context.svn_path_canonicalize(m.module)
466            m.url = client_context.urljoin(self.url, self.offset, m.module)
467
468            if urlparse.urlparse(m.url)[0] not in client_context.schemes:
469                error('%s: Not a valid Subversion URL' % m.url)
470                sc += 1; continue
471#             print >>sys.stderr, '%s: URL constructed from %s %s %s' % \
472#                   (m.url, `self.url`, `self.offset`, `m.module`)
473
474            m.package = posixpath.basename(m.url)
475
476            if self.directory is not None:
477                m.path = os.path.normpath(self.directory)
478            else:
479                scheme, netloc, path, query, fragment = urlparse.urlsplit(m.module)
480                if not scheme and not netloc:
481                    m.path = os.path.normpath(os.path.join(*path.split(posixpath.sep)))
482                else:
483                    m.path = posixpath.basename(m.url)
484
485            if self.version is None:
486                m.head = True
487                m.version = self.trunk_tag(m, client_context) or \
488                            cmt_context.eval_head_version(m)
489#                print >>sys.stderr, 'set version: %s' % m.version
490            else:
491                m.head = False
492                m.version = self.version
493
494            if m.head:
495                m.URL = [posixpath.join(m.url, self.trunk)]
496            else:
497                m.URL = [posixpath.join(m.url, p, m.version)
498                         for p in (self.tags, self.branches)]
499            m.URL = [client_context.svn_path_canonicalize(url) for url in m.URL]
500#            m.URL = [core.svn_path_canonicalize(url) for url in m.URL]
501
502            if cmt_context.with_version_directory:
503                if self.version_dir is None:
504                    m.path = os.path.join(m.path, m.version)
505                else:
506                    m.path = os.path.join(m.path, self.version_dir)
507
508            m.init = True
509#            print m.URL, m.path, m.init
510           
511#        for m in self.modules:
512#            print m.url, m.path, m.init
513        return sc
514
515    def execute(self, cmt_context, client_context):
516        sce = 0
517
518        for m in self.modules:
519            if not m.init: continue
520            done = False
521            err = []
522            for url in m.URL:
523                cmd = 'svn checkout %s %s' % (url, m.path)
524#                cmd = 'svn checkout -q %s %s' % (url, m.path)
525                sc, e = Utils.getstatuserror(cmd)
526#                 cmd = 'svn checkout -q %s %s' % (url, m.path)
527#                 sc, e = Utils.getstatusoutput(cmd)
528                if 0 == sc:
529#                 try:
530#                     #print 'client.checkout2:', url, m.path
531#                     result_rev = client.checkout2(url,
532#                                                   m.path,
533#                                                   self.head_revision,
534#                                                   self.head_revision,
535#                                                   True,
536#                                                   True,
537#                                                   client_context())
538#                 except core.SubversionException, e:
539#                     err.append(e)
540#                     continue
541                    done = True
542                    break
543                else:
544                    err.append(e)
545                    continue
546
547            if not done:
548                for e in err:
549                    error(e)
550#                     #print >> sys.stderr, e
551#                     sc += 1
552#                 print >> sys.stderr, 'Failed to checkout %s into %s.' % \
553#                       (' or '.join(m.URL), m.path)
554                sce += 1
555
556#            print 'Checked out revision %i.' % result_rev
557            scc = cmt_context.configure(m.path, m.version)
558            if scc != 0:
559                print >> sys.stderr, \
560                      '%s %s: configure returned %i.' % (m.path, m.version, scc)
561            sce += scc
562
563        return sce
564
565def main(argv=[__name__]):
566    self = os.path.basename(argv[0])
567    try:
568        opts, args = getopt.getopt(argv[1:],
569                                   "hr:d:o:",
570                                   ["help", "version", "version-tag=",
571                                    "version-dir=", "directory=",
572                                    "offset=", "no_config",
573                                    "with_version_directory",
574                                    "without_version_directory", "url="])
575    except getopt.error, e:
576        print >>sys.stderr, '%s: %s' % (self, str(e))
577        print >>sys.stderr, "Try '%s --help' for more information." % self
578        return 1
579
580    cmt_context = CmtContext()
581    checkout = Checkout()
582
583    for o, v in opts:
584        if o in ("-h", "--help"):
585            print sys.modules[__name__].__doc__
586            return 0
587        elif o in ("--version",):
588            print '%s %s (%s)' % (self, __version__, __date__)
589            print '%sWritten by %s.' % (os.linesep, __author__)
590            return 0
591        elif o in ("-r", "--version-tag"):
592            checkout.version = v
593        elif o in ("--version-dir",):
594            checkout.version_dir = v
595        elif o in ("-d", "--directory"):
596            checkout.directory = v
597        elif o in ("-o", "--offset"):
598            checkout.offset = v
599        elif o in ("--no_config",):
600            cmt_context.config = False
601        elif o in ("--without_version_directory",):
602            cmt_context.with_version_directory = False
603        elif o in ("--with_version_directory",):
604            cmt_context.with_version_directory = True
605        elif o in ("--url",):
606            checkout.url = v
607
608    if not args:
609        print >>sys.stderr, '%s: missing path argument' % self
610        print >>sys.stderr, "Try '%s --help' for more information." % self
611        return 1
612
613    for arg in args:
614        checkout.add(Module(arg))
615
616    client_context = ClientContext()
617    sci = checkout.initialize(cmt_context, client_context)
618    sce = checkout.execute(cmt_context, client_context)
619
620    if sci != 0 or sce !=0:
621        return 1
622    else:
623        return 0
624
625if __name__ == '__main__':
626    sys.exit(main(sys.argv))
Note: See TracBrowser for help on using the repository browser.