Initial commit
[ta/rpmbuilder.git] / rpmbuilder / version_control.py
1 # Copyright 2019 Nokia
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """
16 Module for handling git repository clones
17 """
18 import logging
19 import os
20 import re
21 import subprocess
22 from time import strftime, localtime
23
24 from rpmbuilder.baseerror import RpmbuilderError
25
26
27 class VersionControlSystem(object):
28     """ Handling of project's repositories """
29
30     def __init__(self, clone_target_dir):
31         self.logger = logging.getLogger(__name__)
32         self.clone_target_dir = clone_target_dir
33         self.citag = None
34         self.commitsha = None
35         self.commitauth = None
36         self.commitepocdate = None
37         self.commitmessage = None
38         self.describe = None
39         try:
40             self.__store_head_state()
41         except VcsError:
42             pass
43
44     def update_git_project(self, url, usergivenref):
45         """ Update of a single repository based on given reference """
46         self.logger.info("%-18s: %s", "Git cloning from", url)
47         self.logger.info("%-18s: %s", "Git cloning to", self.clone_target_dir)
48         self.logger.info("%-18s: %s", "Git reference", usergivenref)
49
50         # Check if we already have local clone of the repository
51         self.__clone_repo(url)
52
53         # Change to user given ref value.
54         self.__update_head(url, usergivenref)
55
56         self.__store_head_state()
57         self.citag = self.get_citag()
58
59     def __clone_repo(self, url):
60         """ Create a clone from URL. If already exists, update it """
61         if not os.path.isdir(self.clone_target_dir):
62             self.logger.debug("Creating a fresh clone")
63             cmd = ['git', 'clone', url, self.clone_target_dir]
64             self.logger.debug(self.__run_git(cmd))
65         else:
66             self.logger.debug("We already have a clone. Using old clone.")
67             # Remove any possible garbage from clone directory
68             self.logger.debug("Running cleaning of existing repository")
69             cmd = ['git', 'reset', '--hard']
70             self.logger.debug(self.__run_git(cmd, self.clone_target_dir))
71             # Verify that correct remote is being used
72             self.__set_remoteurl(url)
73             # Run fetch twice. From Git 1.9 onwards this is not necessary,
74             # but to make sure of all server compatibility we do it twice
75             self.logger.debug("Fetching latest from remote")
76             cmd = ['git', 'fetch', 'origin']
77             self.logger.debug(self.__run_git(cmd, self.clone_target_dir))
78             cmd = ['git', 'fetch', 'origin', '--tags']
79             self.logger.debug(self.__run_git(cmd, self.clone_target_dir))
80
81     def __update_head(self, url, usergivenref):
82         """ Change head to point to given ref. Ref can also be tag/commit """
83         self.logger.debug("Reseting git head to %s", usergivenref)
84         try:
85             self.logger.debug("Checking out %s as reference", usergivenref)
86             cmd = ['git', 'checkout', '--force', '--detach', 'origin/' + usergivenref]
87             self.logger.debug(self.__run_git(cmd, self.clone_target_dir))
88         except:
89             self.logger.debug("Unable to checkout %s as reference", usergivenref)
90             try:
91                 self.logger.debug("Checking out %s as tag/commit", usergivenref)
92                 cmd = ['git', 'checkout', '--force', '--detach', usergivenref]
93                 self.logger.debug(self.__run_git(cmd, self.clone_target_dir))
94             except GitError:
95                 raise VcsError(
96                     "Could not checkout branch/ref/commit \"%s\" from %s." % (usergivenref, url))
97
98     def __run_git(self, gitcmd, gitcwd=None):
99         """ Run given git command """
100         assert gitcmd
101         self.logger.debug("Running \'%s\' under directory %s", " ".join(gitcmd), gitcwd)
102         try:
103             return subprocess.check_output(gitcmd,
104                                            shell=False,
105                                            cwd=gitcwd)
106         except subprocess.CalledProcessError as err:
107             raise GitError("Could not execute %s command. Return code was %d" % (err.cmd,
108                                                                                  err.returncode))
109         except:
110             raise
111
112     def __set_remoteurl(self, url):
113         """
114         Verify that repository is using the correct remote URL. If not
115         then it should be changed to the desired one.
116         """
117         self.logger.info("Verifying we have correct remote repository configured")
118         cmd = ["git", "config", "--get", "remote.origin.url"]
119         existing_clone_url = self.__run_git(cmd, self.clone_target_dir).strip()
120         if existing_clone_url != url:
121             self.logger.info("Existing repo has url: %s", existing_clone_url)
122             self.logger.info("Changing repo url to: %s", url)
123             cmd = ["git", "remote", "set-url", "origin", url]
124             self.logger.debug(self.__run_git(cmd, self.clone_target_dir))
125
126     def __store_head_state(self):
127         """ Read checkout values to be used elsewhere """
128         self.logger.info("State of the checkout:")
129
130         try:
131             cmd = ["git", "log", "-1", "--pretty=%H"]
132             self.commitsha = self.__run_git(cmd, self.clone_target_dir).strip()
133             self.logger.info("  %-10s: %s", "SHA", self.commitsha)
134
135             cmd = ["git", "log", "-1", "--pretty=%ae"]
136             self.commitauth = self.__run_git(cmd, self.clone_target_dir).strip()
137             self.logger.info("  %-10s: %s", "Author", self.commitauth)
138
139             cmd = ["git", "log", "-1", "--pretty=%ct"]
140             self.commitepocdate = float(self.__run_git(cmd, self.clone_target_dir).strip())
141             self.logger.info("  %-10s: %s", "Date:",
142                              strftime("%a, %d %b %Y %H:%M:%S",
143                                       localtime(self.commitepocdate)))
144
145             cmd = ["git", "log", "-1", "--pretty=%B"]
146             self.commitmessage = self.__run_git(cmd, self.clone_target_dir).strip()
147             self.logger.info("  %-10s: %s", "Message:", self.commitmessage.split('\n', 1)[0])
148         except GitError:
149             raise VcsError("Directory \"%s\" does not come from vcs" % self.clone_target_dir)
150
151     def is_dirty(self):
152         """ Check the status of directory. Return true if version control is dirty.
153         Git clone is dirty if status shows anything """
154         cmd = ["git", "status", "--porcelain"]
155         return len(self.__run_git(cmd, self.clone_target_dir).strip()) > 0
156
157     def get_citag(self):
158         """ This is for creating the tag for the rpm. """
159
160         if self.citag:
161             return self.citag
162
163         setup_py = os.path.join(self.clone_target_dir, 'setup.py')
164         if os.path.exists(setup_py):
165             with open(setup_py, 'r') as fpoint:
166                 if re.search(r'^.*setup_requires=.*pbr.*$', fpoint.read(), re.MULTILINE):
167                     cmd = ['python', 'setup.py', '--version']
168                     citag = self.__run_git(cmd, self.clone_target_dir).strip()
169                     if ' ' in citag or '\n' in citag:
170                         # 1st execution output may contains extra stuff such as locally installed eggs
171                         citag = self.__run_git(cmd, self.clone_target_dir).strip()
172                     return citag
173
174         try:
175             cmd = ["git", "describe", "--dirty", "--tags"]
176             describe = self.__run_git(cmd, self.clone_target_dir).strip()
177             self.logger.debug("Git describe from tags: %s", describe)
178             if re.search("-", describe):
179                 # if describe format is 2.3-3-g4324323, we need to modify it
180                 dmatch = re.match('^(.*)-([0-9]+)-(g[a-f0-9]{7,}).*$', describe)
181                 if dmatch:
182                     citag = describe.replace('-', '-c', 1)
183                 else:
184                     raise Exception('no match, falling back to non-tagged describe')
185             else:
186                 # if describe format is 2.3
187                 citag = describe
188         except:
189             try:
190                 count = self.__run_git(["git", "rev-list", "HEAD", "--count"],
191                                        self.clone_target_dir).strip()
192                 sha = self.__run_git(["git", "describe", "--long", "--always"],
193                                      self.clone_target_dir).strip()
194                 citag = 'c{}.g{}'.format(count, sha)
195             except:
196                 raise VcsError("Could not create a name for the package with git describe")
197         # Replace all remaining '-' characters with '.' from version number
198         if re.search("-", citag):
199             citag = re.sub('-', '.', citag)
200         return citag
201
202
203 class VcsError(RpmbuilderError):
204     """ Exceptions for all version control error situations """
205     pass
206
207
208 class GitError(RpmbuilderError):
209     """ Exceptions for git command errors """
210     pass