Skip to content

Commit 195c1d0

Browse files
committed
Implement explicit use of a hash for git repositories.
Increment the cfg schema to 1.1, and add explicit support for git checkouts via hashes from the config file. Adds new unit tests for some hash related functionality. Adds checkout of hashes to some basic systems tests. Testing: make test - python2/3 - all tests pass
1 parent 12dd743 commit 195c1d0

16 files changed

+253
-70
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,15 @@ The root of the source tree will be referred to as `${SRC_ROOT}` below.
182182

183183
* tag (string) : tag to checkout
184184

185-
This can also be a git SHA-1
185+
* hash (string) : the git hash to checkout. Only applies to git
186+
repositories.
186187

187188
* branch (string) : branch to checkout from the specified
188189
repository. Specifying a branch on a remote repository means that
189190
checkout_externals will checkout the version of the branch in the remote,
190191
not the the version in the local repository (if it exists).
191192

192-
Note: either tag or branch must be supplied, but not both.
193+
Note: one and only one of tag, branch hash must be supplied.
193194

194195
* externals (string) : used to make manage_externals aware of
195196
sub-externals required by an external. This is a relative path to

manic/checkout.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,15 @@ def commandline_arguments(args=None):
207207
208208
* tag (string) : tag to checkout
209209
210-
This can also be a git SHA-1
210+
* hash (string) : the git hash to checkout. Only applies to git
211+
repositories.
211212
212213
* branch (string) : branch to checkout from the specified
213214
repository. Specifying a branch on a remote repository means that
214215
%(prog)s will checkout the version of the branch in the remote,
215216
not the the version in the local repository (if it exists).
216217
217-
Note: either tag or branch must be supplied, but not both.
218+
Note: one and only one of tag, branch hash must be supplied.
218219
219220
* externals (string) : used to make manage_externals aware of
220221
sub-externals required by an external. This is a relative path to

manic/externals_description.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class ExternalsDescription(dict):
176176
PATH = 'local_path'
177177
PROTOCOL = 'protocol'
178178
REPO_URL = 'repo_url'
179+
HASH = 'hash'
179180
NAME = 'name'
180181

181182
PROTOCOL_EXTERNALS_ONLY = 'externals_only'
@@ -197,6 +198,7 @@ class ExternalsDescription(dict):
197198
REPO_URL: 'string',
198199
TAG: 'string',
199200
BRANCH: 'string',
201+
HASH: 'string',
200202
}
201203
}
202204

@@ -269,18 +271,33 @@ def _check_data(self):
269271
self[ext_name][self.REPO][self.PROTOCOL], ext_name)
270272
fatal_error(msg)
271273

274+
if (self[ext_name][self.REPO][self.PROTOCOL] ==
275+
self.PROTOCOL_SVN):
276+
if self.HASH in self[ext_name][self.REPO]:
277+
msg = ('In repo description for "{0}". svn repositories '
278+
'may not include the "hash" keyword.'.format(
279+
ext_name))
280+
fatal_error(msg)
281+
272282
if (self[ext_name][self.REPO][self.PROTOCOL] !=
273283
self.PROTOCOL_EXTERNALS_ONLY):
274284
ref_count = 0
275285
found_refs = ''
276286
if self.TAG in self[ext_name][self.REPO]:
277287
ref_count += 1
278-
found_refs = '"{0}", {1}'.format(
279-
self[ext_name][self.REPO][self.TAG], found_refs)
288+
found_refs = '"{0} = {1}", {2}'.format(
289+
self.TAG, self[ext_name][self.REPO][self.TAG],
290+
found_refs)
280291
if self.BRANCH in self[ext_name][self.REPO]:
281292
ref_count += 1
282-
found_refs = '"{0}", {1}'.format(
283-
self[ext_name][self.REPO][self.BRANCH], found_refs)
293+
found_refs = '"{0} = {1}", {2}'.format(
294+
self.BRANCH, self[ext_name][self.REPO][self.BRANCH],
295+
found_refs)
296+
if self.HASH in self[ext_name][self.REPO]:
297+
ref_count += 1
298+
found_refs = '"{0} = {1}", {2}'.format(
299+
self.HASH, self[ext_name][self.REPO][self.HASH],
300+
found_refs)
284301

285302
if ref_count > 1:
286303
msg = ('Model description is over specified! Only one of '
@@ -322,6 +339,8 @@ def _check_optional(self):
322339
self[field][self.REPO][self.TAG] = EMPTY_STR
323340
if self.BRANCH not in self[field][self.REPO]:
324341
self[field][self.REPO][self.BRANCH] = EMPTY_STR
342+
if self.HASH not in self[field][self.REPO]:
343+
self[field][self.REPO][self.HASH] = EMPTY_STR
325344
if self.REPO_URL not in self[field][self.REPO]:
326345
self[field][self.REPO][self.REPO_URL] = EMPTY_STR
327346

@@ -428,7 +447,7 @@ def __init__(self, model_data):
428447
"""
429448
ExternalsDescription.__init__(self)
430449
self._schema_major = 1
431-
self._schema_minor = 0
450+
self._schema_minor = 1
432451
self._schema_patch = 0
433452
self._input_major, self._input_minor, self._input_patch = \
434453
get_cfg_schema_version(model_data)

manic/repository.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,26 @@ def __init__(self, component_name, repo):
1919
self._protocol = repo[ExternalsDescription.PROTOCOL]
2020
self._tag = repo[ExternalsDescription.TAG]
2121
self._branch = repo[ExternalsDescription.BRANCH]
22+
self._hash = repo[ExternalsDescription.HASH]
2223
self._url = repo[ExternalsDescription.REPO_URL]
2324

2425
if self._url is EMPTY_STR:
2526
fatal_error('repo must have a URL')
2627

27-
if self._tag is EMPTY_STR and self._branch is EMPTY_STR:
28-
fatal_error('repo must have either a branch or a tag element')
28+
if ((self._tag is EMPTY_STR) and (self._branch is EMPTY_STR) and
29+
(self._hash is EMPTY_STR)):
30+
fatal_error('{0} repo must have a branch, tag or hash element')
2931

30-
if self._tag is not EMPTY_STR and self._branch is not EMPTY_STR:
31-
fatal_error('repo cannot have both a tag and a branch element')
32+
ref_count = 0
33+
if self._tag is not EMPTY_STR:
34+
ref_count += 1
35+
if self._branch is not EMPTY_STR:
36+
ref_count += 1
37+
if self._hash is not EMPTY_STR:
38+
ref_count += 1
39+
if ref_count != 1:
40+
fatal_error('repo {0} must have exactly one of '
41+
'tag, branch or hash.'.format(self._name))
3242

3343
def checkout(self, base_dir_path, repo_dir_name, verbosity): # pylint: disable=unused-argument
3444
"""
@@ -63,3 +73,8 @@ def branch(self):
6373
"""Public access of repo branch.
6474
"""
6575
return self._branch
76+
77+
def hash(self):
78+
"""Public access of repo hash.
79+
"""
80+
return self._hash

manic/repository_git.py

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ def compare_refs(current_ref, expected_ref):
229229
expected_ref = "unknown_remote/{0}".format(self._branch)
230230
else:
231231
expected_ref = "{0}/{1}".format(remote_name, self._branch)
232+
elif self._hash:
233+
# NOTE(bja, 2018-03) For comparison purposes, we could
234+
# determine which is longer and check that the short ref
235+
# is a substring of the long ref. But it is simpler to
236+
# just expand both to the full sha and do an exact
237+
# comparison.
238+
_, expected_ref = self._git_revparse_commit(self._hash)
239+
_, current_ref = self._git_revparse_commit(current_ref)
232240
else:
233241
expected_ref = self._tag
234242

@@ -331,48 +339,64 @@ def _checkout_local_ref(self, verbosity):
331339
"""
332340
if self._tag:
333341
ref = self._tag
334-
else:
342+
elif self._branch:
335343
ref = self._branch
344+
else:
345+
ref = self._hash
346+
336347
self._check_for_valid_ref(ref)
337348
self._git_checkout_ref(ref, verbosity)
338349

339350
def _checkout_external_ref(self, verbosity):
340351
"""Checkout the reference from a remote repository
341352
"""
353+
if self._tag:
354+
ref = self._tag
355+
elif self._branch:
356+
ref = self._branch
357+
else:
358+
ref = self._hash
359+
342360
remote_name = self._determine_remote_name()
343361
if not remote_name:
344362
remote_name = self._create_remote_name()
345363
self._git_remote_add(remote_name, self._url)
346364
self._git_fetch(remote_name)
347-
if self._tag:
348-
is_unique_tag, check_msg = self._is_unique_tag(self._tag,
349-
remote_name)
350-
if not is_unique_tag:
351-
msg = ('In repo "{0}": tag "{1}" {2}'.format(
352-
self._name, self._tag, check_msg))
353-
fatal_error(msg)
354-
ref = self._tag
355-
else:
356-
ref = '{0}/{1}'.format(remote_name, self._branch)
365+
366+
# NOTE(bja, 2018-03) we need to send seperate ref and remote
367+
# name to check_for_vaild_ref, but the combined name to
368+
# checkout_ref!
369+
self._check_for_valid_ref(ref, remote_name)
370+
371+
if self._branch:
372+
ref = '{0}/{1}'.format(remote_name, ref)
357373
self._git_checkout_ref(ref, verbosity)
358374

359-
def _check_for_valid_ref(self, ref):
375+
def _check_for_valid_ref(self, ref, remote_name=None):
360376
"""Try some basic sanity checks on the user supplied reference so we
361377
can provide a more useful error message than calledprocess
362378
error...
363379
364380
"""
365381
is_tag = self._ref_is_tag(ref)
366-
is_branch = self._ref_is_branch(ref)
367-
is_commit = self._ref_is_commit(ref)
382+
is_branch = self._ref_is_branch(ref, remote_name)
383+
is_hash = self._ref_is_hash(ref)
368384

369-
is_valid = is_tag or is_branch or is_commit
385+
is_valid = is_tag or is_branch or is_hash
370386
if not is_valid:
371387
msg = ('In repo "{0}": reference "{1}" does not appear to be a '
372-
'valid tag, branch or commit! Please verify the reference '
388+
'valid tag, branch or hash! Please verify the reference '
373389
'name (e.g. spelling), is available from: {2} '.format(
374390
self._name, ref, self._url))
375391
fatal_error(msg)
392+
393+
if is_tag:
394+
is_unique_tag, msg = self._is_unique_tag(ref, remote_name)
395+
if not is_unique_tag:
396+
msg = ('In repo "{0}": tag "{1}" {2}'.format(
397+
self._name, self._tag, msg))
398+
fatal_error(msg)
399+
376400
return is_valid
377401

378402
def _is_unique_tag(self, ref, remote_name):
@@ -388,7 +412,7 @@ def _is_unique_tag(self, ref, remote_name):
388412
"""
389413
is_tag = self._ref_is_tag(ref)
390414
is_branch = self._ref_is_branch(ref, remote_name)
391-
is_commit = self._ref_is_commit(ref)
415+
is_hash = self._ref_is_hash(ref)
392416

393417
msg = ''
394418
is_unique_tag = False
@@ -407,13 +431,13 @@ def _is_unique_tag(self, ref, remote_name):
407431
'exist. Please check the name.')
408432
is_unique_tag = False
409433
else: # not is_tag and not is_branch:
410-
if is_commit:
434+
if is_hash:
411435
# probably a sha1 or HEAD, etc, we call it a tag
412436
msg = 'is ok'
413437
is_unique_tag = True
414438
else:
415439
# undetermined state.
416-
msg = ('does not appear to be a valid tag, branch or commit! '
440+
msg = ('does not appear to be a valid tag, branch or hash! '
417441
'Please check the name and repository.')
418442
is_unique_tag = False
419443

@@ -494,11 +518,32 @@ def _ref_is_commit(self, ref):
494518
error!
495519
"""
496520
is_commit = False
497-
value = self._git_revparse_commit(ref)
521+
value, _ = self._git_revparse_commit(ref)
498522
if value == 0:
499523
is_commit = True
500524
return is_commit
501525

526+
def _ref_is_hash(self, ref):
527+
"""Verify that a reference is a valid hash according to git.
528+
529+
Git doesn't seem to provide an exact way to determine if user
530+
supplied reference is an actual hash. So we verify that the
531+
ref is a valid commit and return the underlying commit
532+
hash. Then check that the commit hash begins with the user
533+
supplied string.
534+
535+
Note: values returned by git_showref_* and git_revparse are
536+
shell return codes, which are zero for success, non-zero for
537+
error!
538+
539+
"""
540+
is_hash = False
541+
status, git_output = self._git_revparse_commit(ref)
542+
if status == 0:
543+
if git_output.strip().startswith(ref):
544+
is_hash = True
545+
return is_hash
546+
502547
def _status_summary(self, stat, repo_dir_path):
503548
"""Determine the clean/dirty status of a git repository
504549
@@ -600,8 +645,9 @@ def _git_revparse_commit(ref):
600645
"""
601646
cmd = ['git', 'rev-parse', '--quiet', '--verify',
602647
'{0}^{1}'.format(ref, '{commit}'), ]
603-
status = execute_subprocess(cmd, status_to_caller=True)
604-
return status
648+
status, git_output = execute_subprocess(cmd, status_to_caller=True,
649+
output_to_caller=True)
650+
return status, git_output
605651

606652
@staticmethod
607653
def _git_status_porcelain_v1z():

manic/utils.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -288,16 +288,6 @@ def execute_subprocess(commands, status_to_caller=False,
288288
return code, otherwise execute_subprocess treats non-zero return
289289
status as an error and raises an exception.
290290
291-
NOTE(bja, 2018-03) if the user doesn't have credentials setup
292-
correctly, then svn and git will prompt for a username/password or
293-
accepting the domain as trusted. We need to detect this and print
294-
enough info for the user to determine what happened and enter the
295-
appropriate information. When we detect some pre-determined
296-
conditions, we turn on screen output so the user can see what is
297-
needed. There doesn't appear to be a way to detect if the user
298-
entered any information in the terminal. So there is no way to
299-
turn off output.
300-
301291
NOTE(bja, 2018-03) we are polling the running process to avoid
302292
having it hang indefinitely if there is input that we don't
303293
detect. Some large checkouts are multiple minutes long. For now we
@@ -308,7 +298,6 @@ def execute_subprocess(commands, status_to_caller=False,
308298
os.getcwd())
309299
logging.info(msg)
310300
logging.info(commands)
311-
return_to_caller = status_to_caller or output_to_caller
312301
try:
313302
ret_value = _poll_subprocess(
314303
commands, status_to_caller, output_to_caller)
@@ -330,15 +319,20 @@ def execute_subprocess(commands, status_to_caller=False,
330319
# simple status check. If returning, it is the callers
331320
# responsibility determine if an error occurred and handle it
332321
# appropriately.
333-
if not return_to_caller:
322+
if status_to_caller and output_to_caller:
323+
ret_value = (error.returncode, error.output)
324+
elif status_to_caller:
325+
ret_value = error.returncode
326+
elif output_to_caller:
327+
ret_value = error.output
328+
else:
334329
msg_context = ('Process did not run successfully; '
335330
'returned status {0}'.format(error.returncode))
336331
msg = failed_command_msg(msg_context, commands,
337332
output=error.output)
338333
logging.error(error)
339334
log_process_output(error.output)
340335
fatal_error(msg)
341-
ret_value = error.returncode
342336

343337
return ret_value
344338

Binary file not shown.
Binary file not shown.

test/repos/simple-ext.git/objects/60/b1cc1a38d63a4bcaa1e767262bbe23dbf9f5f5

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
x��Q� ��{XXdc�7Y`�ۚ�o=�/3��u���o�P�w��6�YB�9�MĜc��&iښy˦K�K9(�)
2+
�Raq�$)�+�|�ȧ��n����Mᜟ��i��k(�|G�����Fk�N�{]��X+��,�� xoC#
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
x5�� B1=��W�� �b�@bf!7dW���E��0L�V�m�ý�c�᲏N=09�%l~�hP?����rPkևЏ)]��5yB����.��mg4��n�s��$*�

0 commit comments

Comments
 (0)