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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
16 Module for handling git repository clones
22 from time import strftime, localtime
24 from rpmbuilder.baseerror import RpmbuilderError
27 class VersionControlSystem(object):
28 """ Handling of project's repositories """
30 def __init__(self, clone_target_dir):
31 self.logger = logging.getLogger(__name__)
32 self.clone_target_dir = clone_target_dir
35 self.commitauth = None
36 self.commitepocdate = None
37 self.commitmessage = None
40 self.__store_head_state()
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)
50 # Check if we already have local clone of the repository
51 self.__clone_repo(url)
53 # Change to user given ref value.
54 self.__update_head(url, usergivenref)
56 self.__store_head_state()
57 self.citag = self.get_citag()
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))
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))
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)
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))
89 self.logger.debug("Unable to checkout %s as reference", usergivenref)
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))
96 "Could not checkout branch/ref/commit \"%s\" from %s." % (usergivenref, url))
98 def __run_git(self, gitcmd, gitcwd=None):
99 """ Run given git command """
101 self.logger.debug("Running \'%s\' under directory %s", " ".join(gitcmd), gitcwd)
103 return subprocess.check_output(gitcmd,
106 except subprocess.CalledProcessError as err:
107 raise GitError("Could not execute %s command. Return code was %d" % (err.cmd,
112 def __set_remoteurl(self, url):
114 Verify that repository is using the correct remote URL. If not
115 then it should be changed to the desired one.
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))
126 def __store_head_state(self):
127 """ Read checkout values to be used elsewhere """
128 self.logger.info("State of the checkout:")
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)
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)
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)))
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])
149 raise VcsError("Directory \"%s\" does not come from vcs" % self.clone_target_dir)
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
158 """ This is for creating the tag for the rpm. """
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()
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)
182 citag = describe.replace('-', '-c', 1)
184 raise Exception('no match, falling back to non-tagged describe')
186 # if describe format is 2.3
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)
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)
203 class VcsError(RpmbuilderError):
204 """ Exceptions for all version control error situations """
208 class GitError(RpmbuilderError):
209 """ Exceptions for git command errors """