diff options
Diffstat (limited to 'meta/lib/oeqa')
-rw-r--r-- | meta/lib/oeqa/utils/gitarchive.py | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/meta/lib/oeqa/utils/gitarchive.py b/meta/lib/oeqa/utils/gitarchive.py new file mode 100644 index 0000000000..fddd608593 --- /dev/null +++ b/meta/lib/oeqa/utils/gitarchive.py | |||
@@ -0,0 +1,173 @@ | |||
1 | # | ||
2 | # Helper functions for committing data to git and pushing upstream | ||
3 | # | ||
4 | # Copyright (c) 2017, Intel Corporation. | ||
5 | # Copyright (c) 2019, Linux Foundation | ||
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 | # | ||
16 | |||
17 | import os | ||
18 | import re | ||
19 | import sys | ||
20 | from oeqa.utils.git import GitRepo, GitError | ||
21 | |||
22 | class ArchiveError(Exception): | ||
23 | """Internal error handling of this script""" | ||
24 | |||
25 | def format_str(string, fields): | ||
26 | """Format string using the given fields (dict)""" | ||
27 | try: | ||
28 | return string.format(**fields) | ||
29 | except KeyError as err: | ||
30 | raise ArchiveError("Unable to expand string '{}': unknown field {} " | ||
31 | "(valid fields are: {})".format( | ||
32 | string, err, ', '.join(sorted(fields.keys())))) | ||
33 | |||
34 | |||
35 | def init_git_repo(path, no_create, bare, log): | ||
36 | """Initialize local Git repository""" | ||
37 | path = os.path.abspath(path) | ||
38 | if os.path.isfile(path): | ||
39 | raise ArchiveError("Invalid Git repo at {}: path exists but is not a " | ||
40 | "directory".format(path)) | ||
41 | if not os.path.isdir(path) or not os.listdir(path): | ||
42 | if no_create: | ||
43 | raise ArchiveError("No git repo at {}, refusing to create " | ||
44 | "one".format(path)) | ||
45 | if not os.path.isdir(path): | ||
46 | try: | ||
47 | os.mkdir(path) | ||
48 | except (FileNotFoundError, PermissionError) as err: | ||
49 | raise ArchiveError("Failed to mkdir {}: {}".format(path, err)) | ||
50 | if not os.listdir(path): | ||
51 | log.info("Initializing a new Git repo at %s", path) | ||
52 | repo = GitRepo.init(path, bare) | ||
53 | try: | ||
54 | repo = GitRepo(path, is_topdir=True) | ||
55 | except GitError: | ||
56 | raise ArchiveError("Non-empty directory that is not a Git repository " | ||
57 | "at {}\nPlease specify an existing Git repository, " | ||
58 | "an empty directory or a non-existing directory " | ||
59 | "path.".format(path)) | ||
60 | return repo | ||
61 | |||
62 | |||
63 | def git_commit_data(repo, data_dir, branch, message, exclude, notes, log): | ||
64 | """Commit data into a Git repository""" | ||
65 | log.info("Committing data into to branch %s", branch) | ||
66 | tmp_index = os.path.join(repo.git_dir, 'index.oe-git-archive') | ||
67 | try: | ||
68 | # Create new tree object from the data | ||
69 | env_update = {'GIT_INDEX_FILE': tmp_index, | ||
70 | 'GIT_WORK_TREE': os.path.abspath(data_dir)} | ||
71 | repo.run_cmd('add .', env_update) | ||
72 | |||
73 | # Remove files that are excluded | ||
74 | if exclude: | ||
75 | repo.run_cmd(['rm', '--cached'] + [f for f in exclude], env_update) | ||
76 | |||
77 | tree = repo.run_cmd('write-tree', env_update) | ||
78 | |||
79 | # Create new commit object from the tree | ||
80 | parent = repo.rev_parse(branch) | ||
81 | git_cmd = ['commit-tree', tree, '-m', message] | ||
82 | if parent: | ||
83 | git_cmd += ['-p', parent] | ||
84 | commit = repo.run_cmd(git_cmd, env_update) | ||
85 | |||
86 | # Create git notes | ||
87 | for ref, filename in notes: | ||
88 | ref = ref.format(branch_name=branch) | ||
89 | repo.run_cmd(['notes', '--ref', ref, 'add', | ||
90 | '-F', os.path.abspath(filename), commit]) | ||
91 | |||
92 | # Update branch head | ||
93 | git_cmd = ['update-ref', 'refs/heads/' + branch, commit] | ||
94 | if parent: | ||
95 | git_cmd.append(parent) | ||
96 | repo.run_cmd(git_cmd) | ||
97 | |||
98 | # Update current HEAD, if we're on branch 'branch' | ||
99 | if not repo.bare and repo.get_current_branch() == branch: | ||
100 | log.info("Updating %s HEAD to latest commit", repo.top_dir) | ||
101 | repo.run_cmd('reset --hard') | ||
102 | |||
103 | return commit | ||
104 | finally: | ||
105 | if os.path.exists(tmp_index): | ||
106 | os.unlink(tmp_index) | ||
107 | |||
108 | |||
109 | def expand_tag_strings(repo, name_pattern, msg_subj_pattern, msg_body_pattern, | ||
110 | keywords): | ||
111 | """Generate tag name and message, with support for running id number""" | ||
112 | keyws = keywords.copy() | ||
113 | # Tag number is handled specially: if not defined, we autoincrement it | ||
114 | if 'tag_number' not in keyws: | ||
115 | # Fill in all other fields than 'tag_number' | ||
116 | keyws['tag_number'] = '{tag_number}' | ||
117 | tag_re = format_str(name_pattern, keyws) | ||
118 | # Replace parentheses for proper regex matching | ||
119 | tag_re = tag_re.replace('(', '\(').replace(')', '\)') + '$' | ||
120 | # Inject regex group pattern for 'tag_number' | ||
121 | tag_re = tag_re.format(tag_number='(?P<tag_number>[0-9]{1,5})') | ||
122 | |||
123 | keyws['tag_number'] = 0 | ||
124 | for existing_tag in repo.run_cmd('tag').splitlines(): | ||
125 | match = re.match(tag_re, existing_tag) | ||
126 | |||
127 | if match and int(match.group('tag_number')) >= keyws['tag_number']: | ||
128 | keyws['tag_number'] = int(match.group('tag_number')) + 1 | ||
129 | |||
130 | tag_name = format_str(name_pattern, keyws) | ||
131 | msg_subj= format_str(msg_subj_pattern.strip(), keyws) | ||
132 | msg_body = format_str(msg_body_pattern, keyws) | ||
133 | return tag_name, msg_subj + '\n\n' + msg_body | ||
134 | |||
135 | def gitarchive(data_dir, git_dir, no_create, bare, commit_msg_subject, commit_msg_body, branch_name, no_tag, tagname, tag_msg_subject, tag_msg_body, exclude, notes, push, keywords, log): | ||
136 | |||
137 | if not os.path.isdir(data_dir): | ||
138 | raise ArchiveError("Not a directory: {}".format(data_dir)) | ||
139 | |||
140 | data_repo = init_git_repo(git_dir, no_create, bare, log) | ||
141 | |||
142 | # Expand strings early in order to avoid getting into inconsistent | ||
143 | # state (e.g. no tag even if data was committed) | ||
144 | commit_msg = format_str(commit_msg_subject.strip(), keywords) | ||
145 | commit_msg += '\n\n' + format_str(commit_msg_body, keywords) | ||
146 | branch_name = format_str(branch_name, keywords) | ||
147 | tag_name = None | ||
148 | if not no_tag and tagname: | ||
149 | tag_name, tag_msg = expand_tag_strings(data_repo, tagname, | ||
150 | tag_msg_subject, | ||
151 | tag_msg_body, keywords) | ||
152 | |||
153 | # Commit data | ||
154 | commit = git_commit_data(data_repo, data_dir, branch_name, | ||
155 | commit_msg, exclude, notes, log) | ||
156 | |||
157 | # Create tag | ||
158 | if tag_name: | ||
159 | log.info("Creating tag %s", tag_name) | ||
160 | data_repo.run_cmd(['tag', '-a', '-m', tag_msg, tag_name, commit]) | ||
161 | |||
162 | # Push data to remote | ||
163 | if push: | ||
164 | cmd = ['push', '--tags'] | ||
165 | # If no remote is given we push with the default settings from | ||
166 | # gitconfig | ||
167 | if push is not True: | ||
168 | notes_refs = ['refs/notes/' + ref.format(branch_name=branch_name) | ||
169 | for ref, _ in notes] | ||
170 | cmd.extend([push, branch_name] + notes_refs) | ||
171 | log.info("Pushing data to remote") | ||
172 | data_repo.run_cmd(cmd) | ||
173 | |||