Skip to content

Commit bb44ff6

Browse files
authored
Merge pull request #1543 from nexB/1439-load-multiple-scans
1439 load multiple scans
2 parents d19003b + f0e2bdb commit bb44ff6

9 files changed

Lines changed: 20361 additions & 11 deletions

File tree

src/scancode/cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ def print_options(ctx, param, value):
320320

321321
@click.option('--from-json',
322322
is_flag=True,
323+
multiple=True,
323324
help='Load codebase from an existing JSON scan',
324325
help_group=CORE_GROUP, sort_order=25, cls=CommandLineOption)
325326

@@ -543,7 +544,12 @@ def echo_func(*_args, **_kwargs):
543544
elif len(input) == 1:
544545
# we received a single input path, so we treat this as a single path
545546
input = input[0] # NOQA
546-
else:
547+
548+
# This is the case where we have a list of inputs, but the list of inputs are not from the
549+
# `from_json` option. If the `from_json` option is available and we have a list of inputs
550+
# from it, we can pass `input` just fine when we create a VirtualCodebase, otherwise we have to
551+
# process `input` below.
552+
elif not from_json:
547553
# we received a several input paths: we can handle this IFF they share
548554
# a common root directory and none is an absolute path
549555

src/scancode/resource.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,19 +1374,15 @@ def __init__(self, location,
13741374
self.resource_attributes = resource_attributes or OrderedDict()
13751375
self.resource_class = None
13761376
self.has_single_resource = False
1377+
self.location = location
13771378

13781379
scan_data = self._get_scan_data(location)
13791380
self._populate(scan_data)
13801381

1381-
def _get_scan_data(self, location):
1382+
def _get_scan_data_helper(self, location):
13821383
"""
1383-
Return scan data loaded from `location` that is either:
1384-
- a path string
1385-
- a JSON string
1386-
- a Python mapping
1384+
Return scan data loaded from `location`, which is a path string
13871385
"""
1388-
if isinstance(location, dict):
1389-
return location
13901386
try:
13911387
return json.loads(location, object_pairs_hook=OrderedDict)
13921388
except:
@@ -1398,6 +1394,33 @@ def _get_scan_data(self, location):
13981394
f, object_pairs_hook=OrderedDict, encoding='utf-8')
13991395
return scan_data
14001396

1397+
def _get_scan_data(self, location):
1398+
"""
1399+
Return scan data loaded from `location` that is either:
1400+
- a path string
1401+
- a JSON string
1402+
- a Python mapping
1403+
1404+
or `location` is a List or a Tuple that contains multiple paths to scans that are to be joined together.
1405+
"""
1406+
if isinstance(location, dict):
1407+
return location
1408+
if isinstance(location, (list, tuple,)):
1409+
combined_scan_data = OrderedDict(headers=[], files=[])
1410+
for loc in location:
1411+
scan_data = self._get_scan_data_helper(loc)
1412+
headers = scan_data.get('headers')
1413+
if headers:
1414+
combined_scan_data['headers'].extend(headers)
1415+
files = scan_data.get('files')
1416+
if files:
1417+
combined_scan_data['files'].extend(files)
1418+
else:
1419+
raise Exception('Input file does not have Resources to import: {}'.format(loc))
1420+
combined_scan_data['headers'] = sorted(combined_scan_data['headers'], key=lambda x: x['start_timestamp'])
1421+
return combined_scan_data
1422+
return self._get_scan_data_helper(location)
1423+
14011424
def _create_empty_resource_data(self):
14021425
"""
14031426
Return a dictionary of Resource fields and their default values.
@@ -1527,9 +1550,15 @@ def _populate(self, scan_data):
15271550
##########################################################
15281551
# Create root resource without setting root data just yet. If we run into the root data
15291552
# while we iterate through `resources_data`, we fill in the data then.
1530-
sample_resource_path = sample_resource_data['path']
1531-
sample_resource_path = sample_resource_path.strip('/')
1532-
root_path = sample_resource_path.split('/')[0]
1553+
1554+
# Create a virtual root if we are merging multiple scans together
1555+
multiple_input = isinstance(self.location, (list, tuple,)) and len(self.location) > 1
1556+
if multiple_input:
1557+
root_path = 'virtual_root'
1558+
else:
1559+
sample_resource_path = sample_resource_data['path']
1560+
sample_resource_path = sample_resource_path.strip('/')
1561+
root_path = sample_resource_path.split('/')[0]
15331562
root_name = root_path
15341563
root_is_file = False
15351564
root_data = self._create_empty_resource_data()
@@ -1541,6 +1570,9 @@ def _populate(self, scan_data):
15411570

15421571
for resource_data in resources_data:
15431572
path = resource_data.get('path')
1573+
# Append virtual_root path to imported Resource path if we are merging multiple scans
1574+
if multiple_input:
1575+
path = os.path.join(root_path, path)
15441576
name = resource_data.get('name', None)
15451577
if not name:
15461578
name = file_name(path)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"files_count": 2,
3+
"files": [
4+
{
5+
"path": "samples",
6+
"type": "directory",
7+
"summary": []
8+
},
9+
{
10+
"path": "samples/NOTICE",
11+
"type": "file",
12+
"summary": []
13+
}
14+
]
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"files_count": 2,
3+
"files": [
4+
{
5+
"path": "thirdparty",
6+
"type": "directory",
7+
"summary": []
8+
},
9+
{
10+
"path": "thirdparty/example.zip",
11+
"type": "file",
12+
"summary": []
13+
}
14+
]
15+
}

0 commit comments

Comments
 (0)