diff --git a/swh/model/cli.py b/swh/model/cli.py index 036844dfc754601f9b3e60b1f68942815af1a33f..82af76f8cd5827167133edc1bb0556145d5d688e 100644 --- a/swh/model/cli.py +++ b/swh/model/cli.py @@ -35,7 +35,7 @@ def pid_of_dir(path): return pids.persistent_identifier(pids.DIRECTORY, object) -def identify_object(obj_type, obj): +def identify_object(obj_type, follow_symlinks, obj): if obj_type == 'auto': if os.path.isfile(obj): obj_type = 'content' @@ -44,29 +44,40 @@ def identify_object(obj_type, obj): else: # shouldn't happen, due to path validation raise click.BadParameter('%s is neither a file nor a directory' % obj) + + path = obj + if follow_symlinks and os.path.islink(obj): + path = os.path.realpath(obj) + pid = None if obj_type == 'content': - pid = pid_of_file(obj) + pid = pid_of_file(path) elif obj_type == 'directory': - pid = pid_of_dir(obj) + pid = pid_of_dir(path) else: # shouldn't happen, due to option validation raise click.BadParameter('invalid object type: ' + obj_type) + # note: we return original obj instead of path here, to preserve user-given + # file name in output return (obj, pid) @click.command() +@click.option('--dereference/--no-dereference', 'follow_symlinks', + default=True, + help='follow (or not) symlinks for OBJECTS passed as arguments ' + + '(default: follow)') +@click.option('--filename/--no-filename', 'show_filename', default=True, + help='show/hide file name (default: show)') @click.option('--type', '-t', 'obj_type', default='auto', type=click.Choice(['auto', 'content', 'directory']), help='type of object to identify (default: auto)') @click.option('--verify', '-v', metavar='PID', type=PidParamType(), help='reference identifier to be compared with computed one') -@click.option('--filename/--no-filename', 'show_filename', default=True, - help='show/hide file name (default: show)') @click.argument('objects', nargs=-1, type=click.Path(exists=True, readable=True, allow_dash=True, path_type=bytes)) -def identify(obj_type, verify, show_filename, objects): +def identify(obj_type, verify, show_filename, follow_symlinks, objects): """Compute the Software Heritage persistent identifier (PID) for the given source code object(s). @@ -92,7 +103,7 @@ def identify(obj_type, verify, show_filename, objects): if verify and len(objects) != 1: raise click.BadParameter('verification requires a single object') - results = map(partial(identify_object, obj_type), objects) + results = map(partial(identify_object, obj_type, follow_symlinks), objects) if verify: pid = next(results)[1] diff --git a/swh/model/tests/test_cli.py b/swh/model/tests/test_cli.py index d8e68b9e21fba992d9fbfa77c89ea2aebea9469e..45dd36f39730a53240ce6678982765a57fadd2fd 100644 --- a/swh/model/tests/test_cli.py +++ b/swh/model/tests/test_cli.py @@ -22,6 +22,10 @@ class TestIdentify(DataMixin, unittest.TestCase): super().setUp() self.runner = CliRunner() + def assertPidOK(self, result, pid): + self.assertEqual(result.exit_code, 0) + self.assertEqual(result.output.split()[0], pid) + def test_content_id(self): """identify file content""" self.make_contents(self.tmpdir_name) @@ -45,6 +49,21 @@ class TestIdentify(DataMixin, unittest.TestCase): self.assertEqual(result.output.split()[0], 'swh:1:dir:e8b0f1466af8608c8a3fb9879db172b887e80759') + def test_symlink(self): + """identify symlink --- both itself and target""" + regular = os.path.join(self.tmpdir_name, b'foo.txt') + link = os.path.join(self.tmpdir_name, b'bar.txt') + open(regular, 'w').write('foo\n') + os.symlink(os.path.basename(regular), link) + + result = self.runner.invoke(cli.identify, [link]) + self.assertPidOK(result, + 'swh:1:cnt:257cc5642cb1a054f08cc83f2d943e56fd3ebe99') + + result = self.runner.invoke(cli.identify, ['--no-dereference', link]) + self.assertPidOK(result, + 'swh:1:cnt:996f1789ff67c0e3f69ef5933a55d54c5d0e9954') + def test_show_filename(self): """filename is shown by default""" self.make_contents(self.tmpdir_name)