重写仓库主干中所有svn:externals的脚本/工具

2 投票
2 回答
1772 浏览
提问于 2025-04-16 20:07

假设你想把整个代码库里所有的绝对svn:externals网址转换成相对网址。

另外,如果你参考了svn:externals文档里的建议(“你应该认真考虑使用明确的版本号...”),那么你可能会发现自己需要定期在代码库的很多地方更新外部链接的新版本。

那么,有什么好的方法可以程序化地更新大量的svn:externals属性呢?

我的解决方案在下面列出了。

2 个回答

0

kostmo的实现(大约十年前提供的)不支持带空格的固定版本号定义。比如在下面这些有效的外部定义中:

   foo/bar -r 1234 http://example.com/repos/zag
   -r 1234 http://example.com/repos/zag foo/bar1

其中的“-r 1234”部分会被当作本地路径解析,而不是版本号的指定。

我下面的实现解决了这个问题:

#!/usr/bin/python3

import re
import sys
import traceback
from urllib.parse import urlparse


class SvnExternalsLine:
   '''Consult https://subversion.apache.org/docs/release-notes/1.5.html#externals for parsing algorithm.
   The old svn:externals format consists of:
     <local directory> [revision] <absolute remote URL>

   The NEW svn:externals format consists of:
     [revision] <absolute or relative remote URL> <local directory>

   Therefore, "relative" remote paths always come *before* the local path.
   When Subversion sees an svn:externals without an absolute URL,
   it takes the first argument as a relative URL and the second as the target directory.
   One complication is the possibility of local paths with spaces.
   We just assume that the remote path cannot have spaces, and treat all other
   tokens (except the revision specifier) as part of the local path.
   '''

   OLD_FORMAT_REGEXP = re.compile(r'^\s*(?P<loc>.*?)(\s*-r\s*(?P<rev>\d+))?\s+(?P<url>\S+)\s*$')
   NEW_FORMAT_REGEXP = re.compile(r'^\s*(-r\s*(?P<rev>\d+)\s*)?(?P<url>\S+)\s+(?P<loc>.*?)\s*$')

   def __init__(self, original_line):
      self.original_line = original_line

      self.pinned_revision_number = None
      self.repo_url = None
      self.local_pathname = None

      is_abs_url = lambda s: urlparse(s).scheme
      is_old = is_abs_url(original_line.split()[-1])
      regexp = SvnExternalsLine.OLD_FORMAT_REGEXP if is_old else SvnExternalsLine.NEW_FORMAT_REGEXP

      m = regexp.fullmatch(original_line)
      self.repo_url = m.group('url')
      self.local_pathname = m.group('loc')
      self.pinned_revision_number = m.group('rev')


   def constructLine(self):
      '''Reconstruct the externals line in the Subversion 1.5+ format'''
      line = f'{self.repo_url} {self.local_pathname}'
      if self.pinned_revision_number is not None:
          line = f'-r{self.pinned_revision_number} {line}'

      return line
4

这是我用来从svn:externals属性的单行中提取部分内容的类:

from urlparse import urlparse
import re
class SvnExternalsLine:
    '''Consult https://subversion.apache.org/docs/release-notes/1.5.html#externals for parsing algorithm.
    The old svn:externals format consists of:
        <local directory> [revision] <absolute remote URL>

    The NEW svn:externals format consists of:
        [revision] <absolute or relative remote URL> <local directory>

    Therefore, "relative" remote paths always come *after* the local path.
    One complication is the possibility of local paths with spaces.
    We just assume that the remote path cannot have spaces, and treat all other
    tokens (except the revision specifier) as part of the local path.
    '''

    REVISION_ARGUMENT_REGEXP = re.compile("-r(\d+)")

    def __init__(self, original_line):
        self.original_line = original_line

        self.pinned_revision_number = None
        self.repo_url = None
        self.local_pathname_components = []

        for token in self.original_line.split():

            revision_match = self.REVISION_ARGUMENT_REGEXP.match(token)
            if revision_match:
                self.pinned_revision_number = int(revision_match.group(1))
            elif urlparse(token).scheme or any(map(lambda p: token.startswith(p), ["^", "//", "/", "../"])):
                self.repo_url = token
            else:
                self.local_pathname_components.append(token)

    # ---------------------------------------------------------------------
    def constructLine(self):
        '''Reconstruct the externals line in the Subversion 1.5+ format'''

        tokens = []

        # Update the revision specifier if one existed
        if self.pinned_revision_number is not None:
            tokens.append( "-r%d" % (self.pinned_revision_number) )

        tokens.append( self.repo_url )
        tokens.extend( self.local_pathname_components )

        if self.repo_url is None:
            raise Exception("Found a bad externals property: %s; Original definition: %s" % (str(tokens), repr(self.original_line)))

        return " ".join(tokens)

我使用pysvn库来递归遍历所有拥有svn:externals属性的目录,然后通过换行符将这个属性值拆分开来,根据解析出的SvnExternalsLine对每一行进行处理。

这个过程必须在本地检出的代码库上进行。下面是如何使用pysvnpropget)来获取externals的:

client.propget( "svn:externals", base_checkout_path, recurse=True)

遍历这个函数的返回值,在每个目录上修改属性后,

client.propset("svn:externals", new_externals_property, path)

撰写回答