Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SVN usage notes

29/Jul/2026

Re-clonning trunk to an existing branch

svn checkout --depth empty svn://server/repo/branches wc
cd wc
svn update --depth empty oldbranch     # brings just this one node into the WC
svn rm oldbranch
svn cp ^/trunk oldbranch
svn commit -m "Re-branch oldbranch from trunk@HEAD"

SVN Credentials

When automating SVN commands always include the flags --no-auth-cache --non-interactive --trust-server-cert If user and pass are defined in env vars add also: --username "%SVN_U%" --password "%SVN_P%"

Create a new repo (server side)

svnadmin create /srv/svn/repos/myapp

nano myapp/conf/svnserve.conf # example:
    [general]
    anon-access = none
    auth-access = write
    password-db = passwd
    realm = myapp

nano myapp/conf/passwd # example
    [users]
    dev1 = password
    dev2 = password

svn mkdir -m "init layout" \
    svn://<server>/myapp/trunk \
    svn://<server>/myapp/branches \
    svn://<server>/myapp/tags

If websvn is used, check if it will have the correct permissions in the file system (if not runs with same user as svnserve)

Keywords

Set svn:keywords Revision on resources/VERSION - tells SVN which keywords to expand in that file. The placeholder test should match $Revision$ - SVN requires both delimiters to recognize and expand a keyword. SVN will expand it to something like $Revision: 1234$ and keep updating it on future commits to that file.

Pinning Revisions

Using @123 or -r123 have differences.
Example: A dependency folder /deps/libmath, and a completely unrelated rewrite living alongside it as /deps/libmath-ng.

r2    /deps/libmath/version.h     = "libmath 1.0"
      /deps/libmath-ng/version.h  = "libmath-ng 0.1"
r123  /deps/libmath/version.h     = "libmath 1.1"   <-- the revision you pin
r124  /deps/libmath-ng/version.h  = "libmath-ng 0.2"
r125  svn mv /deps/libmath    -> /deps/libmath-legacy    (old one retired)
r126  svn mv /deps/libmath-ng -> /deps/libmath           (ng promoted into the name)

Nothing exotic — upstream retired a component and promoted its replacement into the canonical path. Happens constantly.

Result, both commands succeed:

^/deps/libmath/version.h@123    ->  #define LIBMATH_VERSION "libmath 1.1"
-r123 ^/deps/libmath/version.h  ->  #define LIBMATH_VERSION "libmath-ng 0.1"

No error, no warning. The operative-only form went to HEAD, found that /deps/libmath is now the ng line of history, walked that line back to r123, and handed you a different library. Your pin still says 123 and it is still resolving faithfully — to the wrong component.

Setup with export

bash -c "svn export --force http://192.168.1.x/svn/myapp/trunk ci > /dev/null && bash ci/intro.sh"

Commit messages to Jira

    #!/usr/bin/env python3
    """
    The ONLY valid commit identifier is one JIRA key in square brackets:
        [TASK-1234] Headline
        [TASK-1234][TASK-1235] Headline      several identifiers, one per bracket

    Anything else is treated as ordinary text and stays in the title, e.g.
        Fixed the problem (TASK-1234)        parentheses        -> not an identifier
        [TASK-1, TASK-2]                     list in brackets   -> not an identifier
        [ TASK-1 ] / [task-1]                spaces / lowercase -> not an identifier
        TASK-1: Fix                          bare key           -> not an identifier

    Usage as a module:
        from commit_title import CommitTitle
        ct = CommitTitle(msg)
        ct.title()     # -> "This is the headline"
        ct.issues()    # -> ["TASK-1234", "TASK-1235"]

    """

    import re
    import sys

    __all__ = ["CommitTitle"]


    class CommitTitle(object):
        """
        Parse a commit message once; query the result with .title() and .issues().

        Title rules:
        - First line that still has content after [KEY-n] identifiers, bullet
        markers and surrounding punctuation are removed.
        - If the message consists only of identifiers, the title is the keys.
        - If the message is empty, the title is `fallback`.
        - Truncated at a word boundary to `max_len` characters (None = no limit).

        Self-contained: the only external dependency is the `re` module.
        """

        # JIRA key: project key (uppercase letter, then uppercase/digits/_) + '-' + number
        _KEY = r"[A-Z][A-Z0-9_]*-\d+"
        # Valid identifier: exactly one key inside square brackets, no spaces
        _REF_RE = re.compile(r"\[(" + _KEY + r")\]")
        # List markers: "* ", "- ", "+ ", "1. ", "1) "
        _BULLET_RE = re.compile(r"^\s*(?:[*\-+]|\d+[.)])\s+")
        _WS_RE = re.compile(r"\s+")
        # Punctuation left dangling after removing identifiers
        _LEAD_PUNCT = " \t:;,"        # keep leading '.' / '-' (".cproject", "-Werror")
        _TRAIL_PUNCT = " \t-:;,."
        _ELLIPSIS = "..."

        def __init__(self, message, max_len=72, fallback="(no message)"):
            self._message = message or ""
            self._max_len = max_len
            self._fallback = fallback
            self._issues = self._parse_issues(self._message)
            self._title = self._parse_title(self._message)

        # --- public API -------------------------------------------------------
        def title(self):
            """Short one-line title."""
            return self._title

        def issues(self):
            """[KEY-n] identifiers in order of appearance, without duplicates."""
            return list(self._issues)   # copy: callers cannot alter internal state

        def message(self):
            """The original commit message."""
            return self._message

        def __str__(self):
            return self._title

        def __repr__(self):
            return "CommitTitle(title={!r}, issues={!r})".format(self._title, self._issues)

        # --- internals --------------------------------------------------------
        @classmethod
        def _parse_issues(cls, message):
            found = []
            for key in cls._REF_RE.findall(message):
                if key not in found:
                    found.append(key)
            return found

        def _parse_title(self, message):
            for line in message.splitlines():
                text = self._clean_line(line)
                if any(ch.isalnum() for ch in text):
                    return self._truncate(text)
            if self._issues:
                return self._truncate(", ".join(self._issues))
            return self._fallback

        @classmethod
        def _clean_line(cls, line):
            text = cls._BULLET_RE.sub("", line)
            text = cls._REF_RE.sub(" ", text)
            text = cls._BULLET_RE.sub("", text)  # "[TASK-1] - foo" -> "foo"
            text = cls._WS_RE.sub(" ", text)
            return text.lstrip(cls._LEAD_PUNCT).rstrip(cls._TRAIL_PUNCT)

        def _truncate(self, text):
            max_len = self._max_len
            ellipsis = self._ELLIPSIS
            if max_len is None or len(text) <= max_len:
                return text
            limit = max_len - len(ellipsis)
            if limit <= 0:
                return text[:max_len]
            cut = text.rfind(" ", 0, limit + 1)
            if cut < limit // 2:          # no sensible word boundary -> hard cut
                cut = limit
            return text[:cut].rstrip(self._TRAIL_PUNCT) + ellipsis