Skip to content

Commit

Permalink
Merge pull request #116 from sw360/115-project-create-should-be-able-…
Browse files Browse the repository at this point in the history
…to-copy-an-existing-project

feat: add option to copy existing project and update it
  • Loading branch information
tngraf authored Jan 6, 2025
2 parents 1517220 + 0c39031 commit e757077
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 3 deletions.
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
only for CycloneDX 1.6 and later.
* `bom validate` now also uses `-v` and `--forceerror` and uses the same `bom show` functionality
to check for missing purl or source code url.
* until version 2.6.0, `project create` always set the Project Mainline State of a project release either
to SPECIFIC of to the value given by `-pms`. Now **existing** Project Mainline States are kept.
* `project create` has a new parameter `--copy_from` which allows to first create a copy of the given
project and then update the releases based on the contents of the given SBOM.

## 2.6.0

Expand Down
8 changes: 8 additions & 0 deletions capycli/main/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,13 +403,21 @@ def register_options(self) -> None:
help="force an error exit code in case of visual errors",
)

# used by CreateProject
self.parser.add_argument(
"-pms",
"--project-mainline-state",
dest="project_mainline_state",
help="project mainline state for releases in a newly created project",
)

# used by CreateProject
self.parser.add_argument(
"--copy_from",
dest="copy_from",
help="copy the project with the given id and the update it",
)

def read_config(self, filename: str = "", config_string: str = "") -> Dict[str, Any]:
"""
Read configuration from string or config file.
Expand Down
61 changes: 58 additions & 3 deletions capycli/project/create_project.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -------------------------------------------------------------------------------
# Copyright (c) 2019-2024 Siemens
# Copyright (c) 2019-2025 Siemens
# All Rights Reserved.
# Author: [email protected]
#
Expand Down Expand Up @@ -49,6 +49,28 @@ def bom_to_release_list(self, sbom: Bom) -> List[str]:

return linkedReleases

def get_release_project_mainline_states(self, project: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
pms: List[Dict[str, Any]] = []
if not project:
return pms

if "linkedReleases" not in project:
return pms

for release in project["linkedReleases"]: # NOT ["sw360:releases"]
pms_release = release.get("release", "")
if not pms_release:
continue
pms_state = release.get("mainlineState", "OPEN")
pms_relation = release.get("relation", "UNKNOWN")
pms_entry: Dict[str, Any] = {}
pms_entry["release"] = pms_release
pms_entry["mainlineState"] = pms_state
pms_entry["new_relation"] = pms_relation
pms.append(pms_entry)

return pms

def update_project(self, project_id: str, project: Optional[Dict[str, Any]],
sbom: Bom, project_info: Dict[str, Any]) -> None:
"""Update an existing project with the given SBOM"""
Expand All @@ -57,6 +79,7 @@ def update_project(self, project_id: str, project: Optional[Dict[str, Any]],
sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360)

data = self.bom_to_release_list(sbom)
pms = self.get_release_project_mainline_states(project)

ignore_update_elements = ["name", "version"]
# remove elements from list because they are handled separately
Expand Down Expand Up @@ -91,6 +114,20 @@ def update_project(self, project_id: str, project: Optional[Dict[str, Any]],
if not result2:
print_red(" Error updating project!")

if pms and project:
print_text(" Restoring original project mainline states...")
for pms_entry in pms:
update_release = False
for r in project.get("linkedReleases", []):
if r["release"] == pms_entry["release"]:
update_release = True
break

if update_release:
rid = self.client.get_id_from_href(pms_entry["release"])
self.client.update_project_release_relationship(
project_id, rid, pms_entry["mainlineState"], pms_entry["new_relation"], "")

except SW360Error as swex:
if swex.response is None:
print_red(" Unknown error: " + swex.message)
Expand Down Expand Up @@ -277,19 +314,27 @@ def run(self, args: Any) -> None:
print(" -t SW360_TOKEN, use this token for access to SW360")
print(" -oa, --oauth2 this is an oauth2 token")
print(" -url SW360_URL use this URL for access to SW360")
print(" -name NAME, --name NAME name of the project")
print(" -name NAME name of the project")
print(" -version VERSION, version of the project")
print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters")
print(" -old-version previous version")
print(" -source projectinfo.json additional information about the project to be created")
print(" -pms project mainline state for releases in a newly created project")
print(" --copy_from PROJECT_ID copy the project with the given id and the update it")
return

if not args.inputfile:
print_red("No input file (BOM) specified!")
sys.exit(ResultCode.RESULT_COMMAND_ERROR)

if not args.id:
if args.copy_from:
if args.id:
print_red("--copy_from cannot get combined with -id!")
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
if not args.version:
print_red("No version for the new project specified!")
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
elif not args.id:
if not args.name:
print_red("Neither project name nor id specified!")
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
Expand Down Expand Up @@ -340,6 +385,16 @@ def run(self, args: Any) -> None:
print_red("Error reading project information: " + repr(ex))
sys.exit(ResultCode.RESULT_COMMAND_ERROR)

if args.copy_from:
print(f"Creating a copy of project {args.copy_from}...")
try:
project = self.client.duplicate_project(args.copy_from, args.version)
if project:
args.id = self.client.get_id_from_href(project["_links"]["self"]["href"])
except SW360Error as swex:
print_red(" ERROR: unable to copy existing project:" + repr(swex))
sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360)

if args.id:
self.project_id = args.id
elif args.name and args.version:
Expand Down
1 change: 1 addition & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def __init__(self) -> None:
self.github_token: str = ""
self.force_error: bool = False
self.project_mainline_state = ""
self.copy_from = ""


class TestBasePytest:
Expand Down

0 comments on commit e757077

Please sign in to comment.