Repository: osbuild/osbuild-composer
Pull request #5 (2019-09-24 18:13:26)

Add blueprint workspace diff
The list of changes between the committed blueprint and its workspace are returned when a user requests the diff. Each change includes the new(added) or the old(removed) package.

jkozol: > The documentation says that the route also returns differences in Name, Description, and Version. Does cockpit-composer look at those as well?

Thank you for pointing this out. I missed the version change. A blueprints name can't currently be changed and a change of description is not used by the UI.
jkozol: > I'd squash this commit with the other, so we always test the code as it is added.

The commits have been squashed

Patch:
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index acb1e9f6c..d9cc4c6c7 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -462,13 +462,72 @@ func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *h
 	})
 }
 
-func (api *API) blueprintsDiffHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
-	var reply struct {
-		Diff []interface{} `json:"diff"`
+func (api *API) blueprintsDiffHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
+	type pack struct {
+		Package blueprintPackage `json:"Package"`
 	}
 
-	reply.Diff = make([]interface{}, 0)
-	json.NewEncoder(writer).Encode(reply)
+	type diff struct {
+		New *pack `json:"new"`
+		Old *pack `json:"old"`
+	}
+
+	type reply struct {
+		Diffs []diff `json:"diff"`
+	}
+
+	name := params.ByName("blueprint")
+	if len(name) == 0 {
+		statusResponseError(writer, http.StatusNotFound, "no blueprint name given")
+		return
+	}
+	fromCommit := params.ByName("from")
+	if len(fromCommit) == 0 || fromCommit != "NEWEST" {
+		statusResponseError(writer, http.StatusNotFound, "invalid from commit ID given")
+		return
+	}
+	toCommit := params.ByName("to")
+	if len(toCommit) == 0 || toCommit != "WORKSPACE" {
+		statusResponseError(writer, http.StatusNotFound, "invalid to commit ID given")
+		return
+	}
+
+	// Fetch old and new blueprint details from store and return error if not found
+	var oldBlueprint, newBlueprint blueprint
+	if !api.store.getBlueprintCommitted(name, &oldBlueprint) || !api.store.getBlueprint(name, &newBlueprint, nil) {
+		statusResponseError(writer, http.StatusNotFound)
+		return
+	}
+
+	newSlice := newBlueprint.Packages
+	oldMap := make(map[string]blueprintPackage)
+	diffs := []diff{}
+
+	for _, oldPackage := range oldBlueprint.Packages {
+		oldMap[oldPackage.Name] = oldPackage
+	}
+
+	// For each package in new blueprint check if the old one contains it
+	for _, newPackage := range newSlice {
+		oldPackage, found := oldMap[newPackage.Name]
+		// If found remove from old packages map but otherwise create a diff with the added package
+		if found {
+			delete(oldMap, oldPackage.Name)
+			// Create a diff if the versions changed
+			if oldPackage.Version != newPackage.Version {
+				diffs = append(diffs, diff{Old: &pack{oldPackage}, New: &pack{newPackage}})
+			}
+		} else {
+			diffs = append(diffs, diff{Old: nil, New: &pack{newPackage}})
+		}
+	}
+
+	// All packages remaining in the old packages map have been removed in the new blueprint so create a diff
+	for _, oldPackage := range oldMap {
+		diffs = append(diffs, diff{Old: &pack{oldPackage}, New: nil})
+	}
+
+	json.NewEncoder(writer).Encode(reply{diffs})
 }
 
 func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
diff --git a/internal/weldr/store.go b/internal/weldr/store.go
index 58763a705..31fba5055 100644
--- a/internal/weldr/store.go
+++ b/internal/weldr/store.go
@@ -109,6 +109,27 @@ func (s *store) getBlueprint(name string, bp *blueprint, changed *bool) bool {
 	return true
 }
 
+func (s *store) getBlueprintCommitted(name string, bp *blueprint) bool {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	var ok bool
+	*bp, ok = s.Blueprints[name]
+	if !ok {
+		return false
+	}
+
+	// cockpit-composer cannot deal with missing "packages" or "modules"
+	if bp.Packages == nil {
+		bp.Packages = []blueprintPackage{}
+	}
+	if bp.Modules == nil {
+		bp.Modules = []blueprintPackage{}
+	}
+
+	return true
+}
+
 func (s *store) pushBlueprint(bp blueprint) {
 	s.change(func() {
 		delete(s.Workspace, bp.Name)


Test patch:
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index 705491d75..eca8a7ceb 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -135,6 +135,9 @@ func TestBlueprints(t *testing.T) {
 	testRoute(t, api, "GET", "/api/v0/blueprints/info/test", ``,
 		http.StatusOK, `{"blueprints":[{"name":"test","description":"Test","modules":[],"packages":[{"name":"systemd","version":"123"}],"version":"0"}],
 		"changes":[{"name":"test","changed":true}], "errors":[]}`)
+
+	testRoute(t, api, "GET", "/api/v0/blueprints/diff/test/NEWEST/WORKSPACE", ``,
+		http.StatusOK, `{"diff":[{"new":{"Package":{"name":"systemd","version":"123"}},"old":null},{"new":null,"old":{"Package":{"name":"httpd","version":"2.4.*"}}}]}`)
 }
 
 func TestCompose(t *testing.T) {