summaryrefslogtreecommitdiffstats
path: root/tools/workspace_status_release.py
blob: 36535fb70f5d2ca490e2ad34fa75735813ea769a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/usr/bin/env python

# This is a variant of the `workspace_status.py` script that in addition to
# plain `git describe` implements a few heuristics to arrive at more to the
# point stamps for directories. But due to the implemented heuristics, it will
# typically take longer to run (especially if you use lots of plugins that
# come without tags) and might slow down your development cycle when used
# as default.
#
# To use it, simply add
#
#   --workspace_status_command="python ./tools/workspace_status_release.py"
#
# to your bazel command. So for example instead of
#
#   bazel build release.war
#
# use
#
#   bazel build --workspace_status_command="python ./tools/workspace_status_release.py" release.war
#
# Alternatively, you can add
#
#   build --workspace_status_command="python ./tools/workspace_status_release.py"
#
# to `.bazelrc` in your home directory.
#
# If the script exits with non-zero code, it's considered as a failure
# and the output will be discarded.

from __future__ import print_function
import os
import subprocess
import sys
import re

ROOT = os.path.abspath(__file__)
while not os.path.exists(os.path.join(ROOT, 'WORKSPACE')):
    ROOT = os.path.dirname(ROOT)
REVISION_CMD = ['git', 'describe', '--always', '--dirty']


def run(command):
    try:
        return subprocess.check_output(command).strip().decode("utf-8")
    except OSError as err:
        print('could not invoke %s: %s' % (command[0], err), file=sys.stderr)
        sys.exit(1)
    except subprocess.CalledProcessError:
        # ignore "not a git repository error" to report unknown version
        return None


def revision_with_match(pattern=None, prefix=False, all_refs=False,
                        return_unmatched=False):
    """Return a description of the current commit

    Keyword arguments:
    pattern    -- (Default: None) Use only refs that match this pattern.
    prefix     -- (Default: False) If True, the pattern is considered a prefix
                  and does not require an exact match.
    all_refs   -- (Default: False) If True, consider all refs, not just tags.
    return_unmatched -- (Default: False) If False and a pattern is given that
                  cannot be matched, return the empty string. If True, return
                  the unmatched description nonetheless.
    """

    command = REVISION_CMD[:]
    if pattern:
        command += ['--match', pattern + ('*' if prefix else '')]
    if all_refs:
        command += ['--all', '--long']

    description = run(command)

    if pattern and not return_unmatched and not description.startswith(pattern):
        return ''
    return description


def branch_with_match(pattern):
    for ref_kind in ['origin/', 'gerrit/', '']:
        description = revision_with_match(ref_kind + pattern, all_refs=True,
                                          return_unmatched=True)
        for cutoff in ['heads/', 'remotes/', ref_kind]:
            if description.startswith(cutoff):
                description = description[len(cutoff):]
        if description.startswith(pattern):
            return description
    return ''


def revision(template=None):
    if template:
        # We use the version `v2.16.19-1-gec686a6352` as running example for the
        # below comments. First, we split into ['v2', '16', '19']
        parts = template.split('-')[0].split('.')

        # Although we have releases with version tags containing 4 numbers, we
        # treat only the first three numbers for simplicity. See discussion on
        # Ib1681b2730cf2c443a3cb55fe6e282f6484e18de.

        if len(parts) >= 3:
            # Match for v2.16.19
            version_part = '.'.join(parts[0:3])
            description = revision_with_match(version_part)
            if description:
                return description

        if len(parts) >= 2:
            version_part = '.'.join(parts[0:2])

            # Match for v2.16.*
            description = revision_with_match(version_part + '.', prefix=True)
            if description:
                return description

            # Match for v2.16
            description = revision_with_match(version_part)
            if description.startswith(version_part):
                return description

            if template.startswith('v'):
                # Match for stable-2.16 branches
                branch = 'stable-' + version_part[1:]
                description = branch_with_match(branch)
                if description:
                    return description

    # None of the template based methods worked out, so we're falling back to
    # generic matches.

    # Match for master branch
    description = branch_with_match('master')
    if description:
        return description

    # Match for anything that looks like a version tag
    description = revision_with_match('v[0-9].', return_unmatched=True)
    if description.startswith('v'):
        return description

    # Still no good tag, so we re-try without any matching
    return revision_with_match()


# prints the stamps for the current working directory
def print_stamps_for_cwd(name, template):
    workspace_status_script = os.path.join(
        'tools', 'workspace_status_release.py')
    if os.path.isfile(workspace_status_script):
        # directory has own workspace_status_command, so we use stamps from that
        for line in run(["python", workspace_status_script]).split('\n'):
            if re.search("^STABLE_[a-zA-Z0-9().:@/_ -]*$", line):
                print(line)
    else:
        # directory lacks own workspace_status_command, so we create a default
        # stamp
        v = revision(template)
        print('STABLE_BUILD_%s_LABEL %s' % (name.upper(),
                                            v if v else 'unknown'))


# os.chdir is different from plain `cd` in shells in that it follows symlinks
# and does not update the PWD environment. So when using os.chdir to change into
# a symlinked directory from gerrit's `plugins` or `modules` directory, we
# cannot recover gerrit's directory. This prevents the plugins'/modules'
# `workspace_status_release.py` scripts to detect the name they were symlinked
# as (E.g.: it-* plugins sometimes get linked in more than once under different
# names) and to detect gerrit's root directory. To work around this problem, we
# mimic the `cd` of ordinary shells. By using this function, symlink information
# is preserved in the `PWD` environment variable (as it is for example also done
# in bash) and plugin/module `workspace_status_release.py` scripts can pick up
# the needed information from there.
def cd(absolute_path):
    os.environ['PWD'] = absolute_path
    os.chdir(absolute_path)


def print_stamps():
    cd(ROOT)
    gerrit_version = revision()
    print("STABLE_BUILD_GERRIT_LABEL %s" % gerrit_version)
    for kind in ['modules', 'plugins']:
        kind_dir = os.path.join(ROOT, kind)
        for d in os.listdir(kind_dir) if os.path.isdir(kind_dir) else []:
            p = os.path.join(kind_dir, d)
            if os.path.isdir(p):
                cd(p)
                name = os.path.basename(p)
                print_stamps_for_cwd(name, gerrit_version)


if __name__ == '__main__':
    print_stamps()