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

Last change on this file since 562 was 562, checked in by rybkin, 13 years ago

See C.L. 445

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