blob: 913291a99c6c94a1dd97b1be40c2370f657c49de [file] [log] [blame]
Brad Bishop977dc1a2019-02-06 16:01:43 -05001#!/usr/bin/env python3
Brad Bishop6e60e8b2018-02-01 10:27:11 -05002#
3# Helper script for committing data to git and pushing upstream
4#
5# Copyright (c) 2017, Intel Corporation.
6#
7# This program is free software; you can redistribute it and/or modify it
8# under the terms and conditions of the GNU General Public License,
9# version 2, as published by the Free Software Foundation.
10#
11# This program is distributed in the hope it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14# more details.
15#
16import argparse
17import glob
18import json
19import logging
20import math
21import os
22import re
23import sys
24from collections import namedtuple, OrderedDict
25from datetime import datetime, timedelta, tzinfo
26from operator import attrgetter
27
28# Import oe and bitbake libs
29scripts_path = os.path.dirname(os.path.realpath(__file__))
30sys.path.append(os.path.join(scripts_path, 'lib'))
31import scriptpath
32scriptpath.add_bitbake_lib_path()
33scriptpath.add_oe_lib_path()
34
35from oeqa.utils.git import GitRepo, GitError
36from oeqa.utils.metadata import metadata_from_bb
37
38
39# Setup logging
40logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
41log = logging.getLogger()
42
43
44class ArchiveError(Exception):
45 """Internal error handling of this script"""
46
47
48def format_str(string, fields):
49 """Format string using the given fields (dict)"""
50 try:
51 return string.format(**fields)
52 except KeyError as err:
53 raise ArchiveError("Unable to expand string '{}': unknown field {} "
54 "(valid fields are: {})".format(
55 string, err, ', '.join(sorted(fields.keys()))))
56
57
58def init_git_repo(path, no_create, bare):
59 """Initialize local Git repository"""
60 path = os.path.abspath(path)
61 if os.path.isfile(path):
62 raise ArchiveError("Invalid Git repo at {}: path exists but is not a "
63 "directory".format(path))
64 if not os.path.isdir(path) or not os.listdir(path):
65 if no_create:
66 raise ArchiveError("No git repo at {}, refusing to create "
67 "one".format(path))
68 if not os.path.isdir(path):
69 try:
70 os.mkdir(path)
71 except (FileNotFoundError, PermissionError) as err:
72 raise ArchiveError("Failed to mkdir {}: {}".format(path, err))
73 if not os.listdir(path):
74 log.info("Initializing a new Git repo at %s", path)
75 repo = GitRepo.init(path, bare)
76 try:
77 repo = GitRepo(path, is_topdir=True)
78 except GitError:
79 raise ArchiveError("Non-empty directory that is not a Git repository "
80 "at {}\nPlease specify an existing Git repository, "
81 "an empty directory or a non-existing directory "
82 "path.".format(path))
83 return repo
84
85
86def git_commit_data(repo, data_dir, branch, message, exclude, notes):
87 """Commit data into a Git repository"""
88 log.info("Committing data into to branch %s", branch)
89 tmp_index = os.path.join(repo.git_dir, 'index.oe-git-archive')
90 try:
91 # Create new tree object from the data
92 env_update = {'GIT_INDEX_FILE': tmp_index,
93 'GIT_WORK_TREE': os.path.abspath(data_dir)}
94 repo.run_cmd('add .', env_update)
95
96 # Remove files that are excluded
97 if exclude:
98 repo.run_cmd(['rm', '--cached'] + [f for f in exclude], env_update)
99
100 tree = repo.run_cmd('write-tree', env_update)
101
102 # Create new commit object from the tree
103 parent = repo.rev_parse(branch)
104 git_cmd = ['commit-tree', tree, '-m', message]
105 if parent:
106 git_cmd += ['-p', parent]
107 commit = repo.run_cmd(git_cmd, env_update)
108
109 # Create git notes
110 for ref, filename in notes:
111 ref = ref.format(branch_name=branch)
112 repo.run_cmd(['notes', '--ref', ref, 'add',
113 '-F', os.path.abspath(filename), commit])
114
115 # Update branch head
116 git_cmd = ['update-ref', 'refs/heads/' + branch, commit]
117 if parent:
118 git_cmd.append(parent)
119 repo.run_cmd(git_cmd)
120
121 # Update current HEAD, if we're on branch 'branch'
122 if not repo.bare and repo.get_current_branch() == branch:
123 log.info("Updating %s HEAD to latest commit", repo.top_dir)
124 repo.run_cmd('reset --hard')
125
126 return commit
127 finally:
128 if os.path.exists(tmp_index):
129 os.unlink(tmp_index)
130
131
132def expand_tag_strings(repo, name_pattern, msg_subj_pattern, msg_body_pattern,
133 keywords):
134 """Generate tag name and message, with support for running id number"""
135 keyws = keywords.copy()
136 # Tag number is handled specially: if not defined, we autoincrement it
137 if 'tag_number' not in keyws:
138 # Fill in all other fields than 'tag_number'
139 keyws['tag_number'] = '{tag_number}'
140 tag_re = format_str(name_pattern, keyws)
141 # Replace parentheses for proper regex matching
142 tag_re = tag_re.replace('(', '\(').replace(')', '\)') + '$'
143 # Inject regex group pattern for 'tag_number'
144 tag_re = tag_re.format(tag_number='(?P<tag_number>[0-9]{1,5})')
145
146 keyws['tag_number'] = 0
147 for existing_tag in repo.run_cmd('tag').splitlines():
148 match = re.match(tag_re, existing_tag)
149
150 if match and int(match.group('tag_number')) >= keyws['tag_number']:
151 keyws['tag_number'] = int(match.group('tag_number')) + 1
152
153 tag_name = format_str(name_pattern, keyws)
154 msg_subj= format_str(msg_subj_pattern.strip(), keyws)
155 msg_body = format_str(msg_body_pattern, keyws)
156 return tag_name, msg_subj + '\n\n' + msg_body
157
158
159def parse_args(argv):
160 """Parse command line arguments"""
161 parser = argparse.ArgumentParser(
162 description="Commit data to git and push upstream",
163 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
164
165 parser.add_argument('--debug', '-D', action='store_true',
166 help="Verbose logging")
167 parser.add_argument('--git-dir', '-g', required=True,
168 help="Local git directory to use")
169 parser.add_argument('--no-create', action='store_true',
170 help="If GIT_DIR is not a valid Git repository, do not "
171 "try to create one")
172 parser.add_argument('--bare', action='store_true',
173 help="Initialize a bare repository when creating a "
174 "new one")
175 parser.add_argument('--push', '-p', nargs='?', default=False, const=True,
176 help="Push to remote")
177 parser.add_argument('--branch-name', '-b',
178 default='{hostname}/{branch}/{machine}',
179 help="Git branch name (pattern) to use")
180 parser.add_argument('--no-tag', action='store_true',
181 help="Do not create Git tag")
182 parser.add_argument('--tag-name', '-t',
183 default='{hostname}/{branch}/{machine}/{commit_count}-g{commit}/{tag_number}',
184 help="Tag name (pattern) to use")
185 parser.add_argument('--commit-msg-subject',
186 default='Results of {branch}:{commit} on {hostname}',
187 help="Subject line (pattern) to use in the commit message")
188 parser.add_argument('--commit-msg-body',
189 default='branch: {branch}\ncommit: {commit}\nhostname: {hostname}',
190 help="Commit message body (pattern)")
191 parser.add_argument('--tag-msg-subject',
192 default='Test run #{tag_number} of {branch}:{commit} on {hostname}',
193 help="Subject line (pattern) of the tag message")
194 parser.add_argument('--tag-msg-body',
195 default='',
196 help="Tag message body (pattern)")
197 parser.add_argument('--exclude', action='append', default=[],
198 help="Glob to exclude files from the commit. Relative "
199 "to DATA_DIR. May be specified multiple times")
200 parser.add_argument('--notes', nargs=2, action='append', default=[],
201 metavar=('GIT_REF', 'FILE'),
202 help="Add a file as a note under refs/notes/GIT_REF. "
203 "{branch_name} in GIT_REF will be expanded to the "
204 "actual target branch name (specified by "
205 "--branch-name). This option may be specified "
206 "multiple times.")
207 parser.add_argument('data_dir', metavar='DATA_DIR',
208 help="Data to commit")
209 return parser.parse_args(argv)
210
Brad Bishop977dc1a2019-02-06 16:01:43 -0500211def get_nested(d, list_of_keys):
212 try:
213 for k in list_of_keys:
214 d = d[k]
215 return d
216 except KeyError:
217 return ""
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500218
219def main(argv=None):
220 """Script entry point"""
221 args = parse_args(argv)
222 if args.debug:
223 log.setLevel(logging.DEBUG)
224
225 try:
226 if not os.path.isdir(args.data_dir):
227 raise ArchiveError("Not a directory: {}".format(args.data_dir))
228
229 data_repo = init_git_repo(args.git_dir, args.no_create, args.bare)
230
231 # Get keywords to be used in tag and branch names and messages
232 metadata = metadata_from_bb()
Brad Bishop977dc1a2019-02-06 16:01:43 -0500233 keywords = {'hostname': get_nested(metadata, ['hostname']),
234 'branch': get_nested(metadata, ['layers', 'meta', 'branch']),
235 'commit': get_nested(metadata, ['layers', 'meta', 'commit']),
236 'commit_count': get_nested(metadata, ['layers', 'meta', 'commit_count']),
237 'machine': get_nested(metadata, ['config', 'MACHINE'])}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500238
239 # Expand strings early in order to avoid getting into inconsistent
240 # state (e.g. no tag even if data was committed)
241 commit_msg = format_str(args.commit_msg_subject.strip(), keywords)
242 commit_msg += '\n\n' + format_str(args.commit_msg_body, keywords)
243 branch_name = format_str(args.branch_name, keywords)
244 tag_name = None
245 if not args.no_tag and args.tag_name:
246 tag_name, tag_msg = expand_tag_strings(data_repo, args.tag_name,
247 args.tag_msg_subject,
248 args.tag_msg_body, keywords)
249
250 # Commit data
251 commit = git_commit_data(data_repo, args.data_dir, branch_name,
252 commit_msg, args.exclude, args.notes)
253
254 # Create tag
255 if tag_name:
256 log.info("Creating tag %s", tag_name)
257 data_repo.run_cmd(['tag', '-a', '-m', tag_msg, tag_name, commit])
258
259 # Push data to remote
260 if args.push:
261 cmd = ['push', '--tags']
262 # If no remote is given we push with the default settings from
263 # gitconfig
264 if args.push is not True:
265 notes_refs = ['refs/notes/' + ref.format(branch_name=branch_name)
266 for ref, _ in args.notes]
267 cmd.extend([args.push, branch_name] + notes_refs)
268 log.info("Pushing data to remote")
269 data_repo.run_cmd(cmd)
270
271 except ArchiveError as err:
272 log.error(str(err))
273 return 1
274
275 return 0
276
277if __name__ == "__main__":
278 sys.exit(main())