You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2017/10/27 17:26:00 UTC

[GitHub] pritidesai closed pull request #628: WIP: Adding action annotations while creating that action

pritidesai closed pull request #628: WIP: Adding action annotations while creating that action
URL: https://github.com/apache/incubator-openwhisk-wskdeploy/pull/628
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/deployers/deploymentreader.go b/deployers/deploymentreader.go
index d9e5759..4bd8ef6 100644
--- a/deployers/deploymentreader.go
+++ b/deployers/deploymentreader.go
@@ -21,6 +21,7 @@ import (
 	"github.com/apache/incubator-openwhisk-client-go/whisk"
 	"github.com/apache/incubator-openwhisk-wskdeploy/parsers"
 	"github.com/apache/incubator-openwhisk-wskdeploy/utils"
+	"fmt"
 )
 
 type DeploymentReader struct {
@@ -202,21 +203,29 @@ func (reader *DeploymentReader) bindActionInputsAndAnnotations() {
 				}
 			}
 
-			keyValArr = make(whisk.KeyValueArr, 0)
+			listOfAnnotations := make(whisk.KeyValueArr, 0)
 
 			if len(action.Annotations) > 0 {
-				for name, input := range action.Annotations {
-					var keyVal whisk.KeyValue
+				if wskAction, exists := serviceDeployPack.Actions[actionName]; exists {
+					for name, input := range action.Annotations {
+						if wskAction.Action.Annotations[name] == nil {
+							fmt.Println("WARNING: The annotation key " + name + " does not exist in manifest file")
+							return nil
+						}
+						var keyVal whisk.KeyValue
 
-					keyVal.Key = name
-					keyVal.Value = input
+						keyVal.Key = name
+						keyVal.Value = input
 
-					keyValArr = append(keyValArr, keyVal)
+						listOfAnnotations = append(listOfAnnotations, keyVal)
+					}
+					// appending to already existing annotations
+					// two different set of annotations can be specified in manifest and deployment
+					// therefore do not overwrite annotations from manifest file
+					// merge annotations from both manifest and deployment files
+					wskAction.Action.Annotations = append(wskAction.Action.Annotations , listOfAnnotations...)
 				}
 
-				if wskAction, exists := serviceDeployPack.Actions[actionName]; exists {
-					wskAction.Action.Annotations = keyValArr
-				}
 			}
 		}
 
diff --git a/deployers/deploymentreader_test.go b/deployers/deploymentreader_test.go
index 53e326b..07329ef 100644
--- a/deployers/deploymentreader_test.go
+++ b/deployers/deploymentreader_test.go
@@ -20,9 +20,10 @@
 package deployers
 
 import (
+	"github.com/apache/incubator-openwhisk-client-go/whisk"
 	"github.com/stretchr/testify/assert"
+	"reflect"
 	"testing"
-	"github.com/apache/incubator-openwhisk-client-go/whisk"
 )
 
 var sd *ServiceDeployer
@@ -87,70 +88,107 @@ func TestDeploymentReader_bindTrigger(t *testing.T) {
 }
 
 func TestDeploymentReader_bindTrigger_packages(t *testing.T) {
-    //init variables
-    sDeployer := NewServiceDeployer()
-    sDeployer.DeploymentPath = "../tests/dat/deployment-deploymentreader-test-packages.yml"
-    sDeployer.Deployment.Triggers["locationUpdate"] = new(whisk.Trigger)
-
-    //parse deployment and bind triggers input and annotation
-    dReader := NewDeploymentReader(sDeployer)
-    dReader.HandleYaml()
-    dReader.bindTriggerInputsAndAnnotations()
-
-    trigger := sDeployer.Deployment.Triggers["locationUpdate"]
-    for _, param := range trigger.Parameters {
-        switch param.Key {
-        case "name":
-            assert.Equal(t, "Bernie", param.Value, "Failed to set inputs")
-        case "place":
-            assert.Equal(t, "DC", param.Value, "Failed to set inputs")
-        default:
-            assert.Fail(t, "Failed to get inputs key")
-
-        }
-    }
-    for _, annos := range trigger.Annotations {
-        switch annos.Key {
-        case "bbb":
-            assert.Equal(t, "this is an annotation", annos.Value, "Failed to set annotations")
-        default:
-            assert.Fail(t, "Failed to get annotation key")
-
-        }
-    }
+	//init variables
+	sDeployer := NewServiceDeployer()
+	sDeployer.DeploymentPath = "../tests/dat/deployment-deploymentreader-test-packages.yml"
+	sDeployer.Deployment.Triggers["locationUpdate"] = new(whisk.Trigger)
+
+	//parse deployment and bind triggers input and annotation
+	dReader := NewDeploymentReader(sDeployer)
+	dReader.HandleYaml()
+	dReader.bindTriggerInputsAndAnnotations()
+
+	trigger := sDeployer.Deployment.Triggers["locationUpdate"]
+	for _, param := range trigger.Parameters {
+		switch param.Key {
+		case "name":
+			assert.Equal(t, "Bernie", param.Value, "Failed to set inputs")
+		case "place":
+			assert.Equal(t, "DC", param.Value, "Failed to set inputs")
+		default:
+			assert.Fail(t, "Failed to get inputs key")
+
+		}
+	}
+	for _, annos := range trigger.Annotations {
+		switch annos.Key {
+		case "bbb":
+			assert.Equal(t, "this is an annotation", annos.Value, "Failed to set annotations")
+		default:
+			assert.Fail(t, "Failed to get annotation key")
+
+		}
+	}
 }
 
 func TestDeploymentReader_bindTrigger_package(t *testing.T) {
-    //init variables
-    sDeployer := NewServiceDeployer()
-    sDeployer.DeploymentPath = "../tests/dat/deployment-deploymentreader-test-package.yml"
-    sDeployer.Deployment.Triggers["locationUpdate"] = new(whisk.Trigger)
-
-    //parse deployment and bind triggers input and annotation
-    dReader := NewDeploymentReader(sDeployer)
-    dReader.HandleYaml()
-    dReader.bindTriggerInputsAndAnnotations()
-
-    assert.Equal(t, "triggerrule", dReader.DeploymentDescriptor.Package.Packagename)
-    trigger := sDeployer.Deployment.Triggers["locationUpdate"]
-    for _, param := range trigger.Parameters {
-        switch param.Key {
-        case "name":
-            assert.Equal(t, "Bernie", param.Value, "Failed to set inputs")
-        case "place":
-            assert.Equal(t, "DC", param.Value, "Failed to set inputs")
-        default:
-            assert.Fail(t, "Failed to get inputs key")
-
-        }
-    }
-    for _, annos := range trigger.Annotations {
-        switch annos.Key {
-        case "bbb":
-            assert.Equal(t, "this is an annotation", annos.Value, "Failed to set annotations")
-        default:
-            assert.Fail(t, "Failed to get annotation key")
-
-        }
-    }
+	//init variables
+	sDeployer := NewServiceDeployer()
+	sDeployer.DeploymentPath = "../tests/dat/deployment-deploymentreader-test-package.yml"
+	sDeployer.Deployment.Triggers["locationUpdate"] = new(whisk.Trigger)
+
+	//parse deployment and bind triggers input and annotation
+	dReader := NewDeploymentReader(sDeployer)
+	dReader.HandleYaml()
+	dReader.bindTriggerInputsAndAnnotations()
+
+	assert.Equal(t, "triggerrule", dReader.DeploymentDescriptor.Package.Packagename)
+	trigger := sDeployer.Deployment.Triggers["locationUpdate"]
+	for _, param := range trigger.Parameters {
+		switch param.Key {
+		case "name":
+			assert.Equal(t, "Bernie", param.Value, "Failed to set inputs")
+		case "place":
+			assert.Equal(t, "DC", param.Value, "Failed to set inputs")
+		default:
+			assert.Fail(t, "Failed to get inputs key")
+
+		}
+	}
+	for _, annos := range trigger.Annotations {
+		switch annos.Key {
+		case "bbb":
+			assert.Equal(t, "this is an annotation", annos.Value, "Failed to set annotations")
+		default:
+			assert.Fail(t, "Failed to get annotation key")
+
+		}
+	}
+}
+
+func TestDeploymentReader_BindAssets_ActionAnnotations(t *testing.T) {
+	sDeployer := NewServiceDeployer()
+	sDeployer.DeploymentPath = "../tests/dat/deployment_validate_action_annotations.yaml"
+	sDeployer.ManifestPath = "../tests/dat/manifest_validate_action_annotations.yaml"
+
+	//parse deployment and bind triggers input and annotation
+	dReader := NewDeploymentReader(sDeployer)
+	dReader.HandleYaml()
+	dReader.bindActionInputsAndAnnotations()
+
+	pkg_name := "packageActionAnnotations"
+	pkg := dReader.DeploymentDescriptor.Packages[pkg_name]
+	assert.NotNil(t, pkg, "Could not find package with name "+pkg_name)
+	action_name := "helloworld"
+	action := dReader.DeploymentDescriptor.GetProject().Packages[pkg_name].Actions[action_name]
+	assert.NotNil(t, action, "Could not find action with name "+action_name)
+	actual_annotations := action.Annotations
+	expected_annotations := map[string]interface{}{
+		"action_annotation_5": "this is annotation 5",
+		"action_annotation_6": "this is annotation 6",
+	}
+	assert.Equal(t, len(actual_annotations), len(expected_annotations), "Could not find expected number of annotations specified in manifest file")
+	eq := reflect.DeepEqual(actual_annotations, expected_annotations)
+	assert.True(t, eq, "Expected list of annotations does not match with actual list, expected annotations: %v actual annotations: %v", expected_annotations, actual_annotations)
+
+	pkg_name = "packageActionAnnotationsWithWebAction"
+	pkg = dReader.DeploymentDescriptor.Packages[pkg_name]
+	assert.NotNil(t, pkg, "Could not find package with name "+pkg_name)
+	action = dReader.DeploymentDescriptor.GetProject().Packages[pkg_name].Actions[action_name]
+	assert.NotNil(t, action, "Could not find action with name "+action_name)
+	actual_annotations = action.Annotations
+	expected_annotations["web-export"] = true
+	assert.Equal(t, len(actual_annotations), len(expected_annotations), "Could not find expected number of annotations specified in manifest file")
+	eq = reflect.DeepEqual(actual_annotations, expected_annotations)
+	assert.True(t, eq, "Expected list of annotations does not match with actual list, expected annotations: %v actual annotations: %v", expected_annotations, actual_annotations)
 }
diff --git a/parsers/manifest_parser.go b/parsers/manifest_parser.go
index 5aa98e7..bb55e2a 100644
--- a/parsers/manifest_parser.go
+++ b/parsers/manifest_parser.go
@@ -523,24 +523,22 @@ func (dm *YAMLParser) ComposeActions(filePath string, actions map[string]Action,
 		/*
  		 *  Action.Annotations
  		 */
-		keyValArr = make(whisk.KeyValueArr, 0)
+		listOfAnnotations := make(whisk.KeyValueArr, 0)
 		for name, value := range action.Annotations {
 			var keyVal whisk.KeyValue
 			keyVal.Key = name
 			keyVal.Value = utils.GetEnvVar(value)
-			keyValArr = append(keyValArr, keyVal)
-			// TODO{} Fix Annottions; they are not added to Action if web-export key is not present
-			// Need to assure annotations are added/set even if web-export is not set on the action.
+			listOfAnnotations = append(listOfAnnotations, keyVal)
+		}
+		if len(listOfAnnotations) > 0 {
+			wskaction.Annotations = append(wskaction.Annotations, listOfAnnotations...)
 		}
 
 		/*
   		 *  Web Export
   		 */
-		// only set the webaction when the annotations are not empty.
 		if action.Webexport == "true" {
-			// TODO() why is this commented out?  we should now support annotations...
-			//wskaction.Annotations = keyValArr
-			wskaction.Annotations, errorParser = utils.WebAction("yes", keyValArr, action.Name, false)
+			wskaction.Annotations, errorParser = utils.WebAction("yes", listOfAnnotations, false)
 			if errorParser != nil {
 				return s1, errorParser
 			}
diff --git a/parsers/manifest_parser_test.go b/parsers/manifest_parser_test.go
index 558cdeb..157a85d 100644
--- a/parsers/manifest_parser_test.go
+++ b/parsers/manifest_parser_test.go
@@ -1595,3 +1595,36 @@ func TestPackageName_Env_Var(t *testing.T) {
         assert.Equal(t, "Apache-2.0", pkg.License, "Get the wrong license.")
     }
 }
+
+func TestComposeActionForAnnotations(t *testing.T) {
+    manifestFile := "../tests/dat/manifest_validate_action_annotations.yaml"
+    mm := NewYAMLParser()
+    manifest, _ := mm.ParseManifest(manifestFile)
+    pkg_name := "packageActionAnnotations"
+    pkg := manifest.Packages[pkg_name]
+    assert.NotNil(t, pkg, "Could not find package with name " + pkg_name)
+    action_name := "helloworld"
+    action := pkg.Actions[action_name]
+    assert.NotNil(t, action, "Could not find action with name " + action_name)
+    actual_annotations := action.Annotations
+    expected_annotations := map[string]interface{} {
+        "action_annotation_1": "this is annotation 1",
+        "action_annotation_2": "this is annotation 2",
+        "action_annotation_3": "this is annotation 3",
+        "action_annotation_4": "this is annotation 4",
+    }
+    assert.Equal(t, len(actual_annotations), len(expected_annotations), "Could not find expected number of annotations specified in manifest file")
+    eq := reflect.DeepEqual(actual_annotations, expected_annotations)
+    assert.True(t, eq, "Expected list of annotations does not match with actual list, expected annotations: %v actual annotations: %v", expected_annotations, actual_annotations)
+
+    pkg_name = "packageActionAnnotationsWithWebAction"
+    pkg = manifest.Packages[pkg_name]
+    assert.NotNil(t, pkg, "Could not find package with name " + pkg_name)
+    action = pkg.Actions[action_name]
+    assert.NotNil(t, action, "Could not find action with name " + action_name)
+    actual_annotations = action.Annotations
+    expected_annotations["web-export"] = true
+    assert.Equal(t, len(actual_annotations), len(expected_annotations), "Could not find expected number of annotations specified in manifest file")
+    eq = reflect.DeepEqual(actual_annotations, expected_annotations)
+    assert.True(t, eq, "Expected list of annotations does not match with actual list, expected annotations: %v actual annotations: %v", expected_annotations, actual_annotations)
+}
diff --git a/tests/dat/deployment_validate_action_annotations.yaml b/tests/dat/deployment_validate_action_annotations.yaml
new file mode 100644
index 0000000..b551e8f
--- /dev/null
+++ b/tests/dat/deployment_validate_action_annotations.yaml
@@ -0,0 +1,22 @@
+project:
+    name: TestActionAnnotations
+    packages:
+        packageActionAnnotations:
+            actions:
+                helloworld:
+                    inputs:
+                        name: Amy
+                        place: New York
+                    annotations:
+                        action_annotation_5: this is annotation 5
+                        action_annotation_6: this is annotation 6
+        packageActionAnnotationsWithWebAction:
+            actions:
+                helloworld:
+                    inputs:
+                        name: Amy
+                        place: New York
+                    annotations:
+                        action_annotation_5: this is annotation 5
+                        action_annotation_6: this is annotation 6
+                        web-export: true
diff --git a/tests/dat/manifest_validate_action_annotations.yaml b/tests/dat/manifest_validate_action_annotations.yaml
new file mode 100644
index 0000000..44b2f8b
--- /dev/null
+++ b/tests/dat/manifest_validate_action_annotations.yaml
@@ -0,0 +1,44 @@
+packages:
+    packageActionAnnotations:
+        actions:
+            helloworld:
+                function: actions/helloworld.js
+                runtime: nodejs:6
+                inputs:
+                    name:
+                        type: string
+                        description: name of a person
+                    place:
+                        type: string
+                        description: location of a person
+                outputs:
+                    payload:
+                        type: string
+                        description: a simple greeting message, Hello World!
+                annotations:
+                    action_annotation_1: this is annotation 1
+                    action_annotation_2: this is annotation 2
+                    action_annotation_3: this is annotation 3
+                    action_annotation_4: this is annotation 4
+    packageActionAnnotationsWithWebAction:
+        actions:
+            helloworld:
+                function: actions/helloworld.js
+                runtime: nodejs:6
+                inputs:
+                    name:
+                        type: string
+                        description: name of a person
+                    place:
+                        type: string
+                        description: location of a person
+                outputs:
+                    payload:
+                        type: string
+                        description: a simple greeting message, Hello World!
+                annotations:
+                    action_annotation_1: this is annotation 1
+                    action_annotation_2: this is annotation 2
+                    action_annotation_3: this is annotation 3
+                    action_annotation_4: this is annotation 4
+                    web-export: true
diff --git a/utils/misc.go b/utils/misc.go
index b37b059..b9d862e 100644
--- a/utils/misc.go
+++ b/utils/misc.go
@@ -331,77 +331,6 @@ func javaEntryError() error {
 	return errors.New(errMsg)
 }
 
-//for web action support, code from wsk cli with tiny adjustments
-const WEB_EXPORT_ANNOT = "web-export"
-const RAW_HTTP_ANNOT = "raw-http"
-const FINAL_ANNOT = "final"
-
-func WebAction(webMode string, annotations whisk.KeyValueArr, entityName string, fetch bool) (whisk.KeyValueArr, error) {
-	switch strings.ToLower(webMode) {
-	case "yes":
-		fallthrough
-	case "true":
-		return webActionAnnotations(fetch, annotations, entityName, addWebAnnotations)
-	case "no":
-		fallthrough
-	case "false":
-		return webActionAnnotations(fetch, annotations, entityName, deleteWebAnnotations)
-	case "raw":
-		return webActionAnnotations(fetch, annotations, entityName, addRawAnnotations)
-	default:
-		return nil, errors.New(webMode)
-	}
-}
-
-type WebActionAnnotationMethod func(annotations whisk.KeyValueArr) whisk.KeyValueArr
-
-func webActionAnnotations(
-	fetchAnnotations bool,
-	annotations whisk.KeyValueArr,
-	entityName string,
-	webActionAnnotationMethod WebActionAnnotationMethod) (whisk.KeyValueArr, error) {
-	if annotations != nil || !fetchAnnotations {
-		annotations = webActionAnnotationMethod(annotations)
-	}
-
-	return annotations, nil
-}
-
-func addWebAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
-	annotations = deleteWebAnnotationKeys(annotations)
-	annotations = addKeyValue(WEB_EXPORT_ANNOT, true, annotations)
-	annotations = addKeyValue(RAW_HTTP_ANNOT, false, annotations)
-	annotations = addKeyValue(FINAL_ANNOT, true, annotations)
-
-	return annotations
-}
-
-func deleteWebAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
-	annotations = deleteWebAnnotationKeys(annotations)
-	annotations = addKeyValue(WEB_EXPORT_ANNOT, false, annotations)
-	annotations = addKeyValue(RAW_HTTP_ANNOT, false, annotations)
-	annotations = addKeyValue(FINAL_ANNOT, false, annotations)
-
-	return annotations
-}
-
-func addRawAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
-	annotations = deleteWebAnnotationKeys(annotations)
-	annotations = addKeyValue(WEB_EXPORT_ANNOT, true, annotations)
-	annotations = addKeyValue(RAW_HTTP_ANNOT, true, annotations)
-	annotations = addKeyValue(FINAL_ANNOT, true, annotations)
-
-	return annotations
-}
-
-func deleteWebAnnotationKeys(annotations whisk.KeyValueArr) whisk.KeyValueArr {
-	annotations = deleteKey(WEB_EXPORT_ANNOT, annotations)
-	annotations = deleteKey(RAW_HTTP_ANNOT, annotations)
-	annotations = deleteKey(FINAL_ANNOT, annotations)
-
-	return annotations
-}
-
 func deleteKey(key string, keyValueArr whisk.KeyValueArr) whisk.KeyValueArr {
 	for i := 0; i < len(keyValueArr); i++ {
 		if keyValueArr[i].Key == key {
diff --git a/utils/webaction.go b/utils/webaction.go
new file mode 100644
index 0000000..f9f6e4e
--- /dev/null
+++ b/utils/webaction.go
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package utils
+
+import (
+	"errors"
+	"github.com/apache/incubator-openwhisk-client-go/whisk"
+	"strings"
+)
+
+//for web action support, code from wsk cli with tiny adjustments
+const WEB_EXPORT_ANNOT = "web-export"
+const RAW_HTTP_ANNOT = "raw-http"
+const FINAL_ANNOT = "final"
+
+func WebAction(webMode string, annotations whisk.KeyValueArr, fetch bool) (whisk.KeyValueArr, error) {
+	switch strings.ToLower(webMode) {
+	case "yes":
+		fallthrough
+	case "true":
+		return webActionAnnotations(fetch, annotations, addWebAnnotations)
+	case "no":
+		fallthrough
+	case "false":
+		return webActionAnnotations(fetch, annotations, deleteWebAnnotations)
+	case "raw":
+		return webActionAnnotations(fetch, annotations, addRawAnnotations)
+	default:
+		return nil, errors.New(webMode)
+	}
+}
+
+type WebActionAnnotationMethod func(annotations whisk.KeyValueArr) whisk.KeyValueArr
+
+func webActionAnnotations(
+	fetchAnnotations bool,
+	annotations whisk.KeyValueArr,
+	webActionAnnotationMethod WebActionAnnotationMethod) (whisk.KeyValueArr, error) {
+	if annotations != nil || !fetchAnnotations {
+		annotations = webActionAnnotationMethod(annotations)
+	}
+
+	return annotations, nil
+}
+
+func addWebAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
+	annotations = deleteWebAnnotationKeys(annotations)
+	annotations = addKeyValue(WEB_EXPORT_ANNOT, true, annotations)
+	annotations = addKeyValue(RAW_HTTP_ANNOT, false, annotations)
+	annotations = addKeyValue(FINAL_ANNOT, true, annotations)
+
+	return annotations
+}
+
+func deleteWebAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
+	annotations = deleteWebAnnotationKeys(annotations)
+	annotations = addKeyValue(WEB_EXPORT_ANNOT, false, annotations)
+	annotations = addKeyValue(RAW_HTTP_ANNOT, false, annotations)
+	annotations = addKeyValue(FINAL_ANNOT, false, annotations)
+
+	return annotations
+}
+
+func addRawAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
+	annotations = deleteWebAnnotationKeys(annotations)
+	annotations = addKeyValue(WEB_EXPORT_ANNOT, true, annotations)
+	annotations = addKeyValue(RAW_HTTP_ANNOT, true, annotations)
+	annotations = addKeyValue(FINAL_ANNOT, true, annotations)
+
+	return annotations
+}
+
+func deleteWebAnnotationKeys(annotations whisk.KeyValueArr) whisk.KeyValueArr {
+	annotations = deleteKey(WEB_EXPORT_ANNOT, annotations)
+	annotations = deleteKey(RAW_HTTP_ANNOT, annotations)
+	annotations = deleteKey(FINAL_ANNOT, annotations)
+
+	return annotations
+}


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services