Check in manifest/ , a small library for reading and writing manifest
files.

Change-Id: Ifee6b01640197f9113d6845c980b1e80f0c5f8bf
diff --git a/manifest/parser.go b/manifest/parser.go
new file mode 100644
index 0000000..0bdde9b
--- /dev/null
+++ b/manifest/parser.go
@@ -0,0 +1,81 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed 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 manifest
+
+import (
+	"encoding/xml"
+	"io/ioutil"
+	"sort"
+	"strings"
+)
+
+func (p *Project) parse() {
+	for _, s := range strings.Split(p.GroupsString, ",") {
+		if s == "" {
+			continue
+		}
+		if p.Groups == nil {
+			p.Groups = map[string]bool{}
+		}
+		p.Groups[s] = true
+	}
+}
+
+func (p *Project) prepare() {
+	var keys []string
+	for k, v := range p.Groups {
+		if v {
+			keys = append(keys, k)
+		}
+	}
+
+	sort.Strings(keys)
+	p.GroupsString = strings.Join(keys, ",")
+}
+
+// Parse parses the given XML data.
+func Parse(contents []byte) (*Manifest, error) {
+	var m Manifest
+	if err := xml.Unmarshal(contents, &m); err != nil {
+		return nil, err
+	}
+
+	for i := range m.Project {
+		m.Project[i].parse()
+	}
+	return &m, nil
+}
+
+// MarshalXML serializes the receiver to XML.
+func (m *Manifest) MarshalXML() ([]byte, error) {
+	for i := range m.Project {
+		m.Project[i].prepare()
+	}
+
+	content, err := xml.MarshalIndent(m, "", " ")
+	if err != nil {
+		return nil, err
+	}
+	return content, nil
+}
+
+// ParseFile reads and parses an XML file
+func ParseFile(name string) (*Manifest, error) {
+	content, err := ioutil.ReadFile(name)
+	if err != nil {
+		return nil, err
+	}
+	return Parse(content)
+}
diff --git a/manifest/parser_test.go b/manifest/parser_test.go
new file mode 100644
index 0000000..0228487
--- /dev/null
+++ b/manifest/parser_test.go
@@ -0,0 +1,114 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed 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 manifest
+
+import (
+	"reflect"
+	"testing"
+)
+
+var aospManifest = `<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <remote  name="aosp"
+           fetch=".."
+           review="https://android-review.googlesource.com/" />
+  <default revision="master"
+           remote="aosp"
+           sync-j="4" />
+
+  <project path="build" name="platform/build" groups="pdk,tradefed" >
+    <copyfile src="core/root.mk" dest="Makefile" />
+  </project>
+  <project path="build/soong" name="platform/build/soong" groups="pdk,tradefed" >
+    <linkfile src="root.bp" dest="Android.bp" />
+  </project>
+</manifest>`
+
+func TestBasic(t *testing.T) {
+	manifest, err := Parse([]byte(aospManifest))
+	if err != nil {
+		t.Fatalf("Unmarshal: %v", err)
+	}
+
+	want := &Manifest{
+		Remote: []Remote{{
+			Name:   "aosp",
+			Fetch:  "..",
+			Review: "https://android-review.googlesource.com/",
+		}},
+		Default: Default{
+			Revision: "master",
+			Remote:   "aosp",
+			SyncJ:    "4",
+		},
+		Project: []Project{
+			{
+				Path:         "build",
+				Name:         "platform/build",
+				GroupsString: "pdk,tradefed",
+				Groups: map[string]bool{
+					"pdk":      true,
+					"tradefed": true,
+				},
+				Copyfile: []Copyfile{
+					{
+						Src:  "core/root.mk",
+						Dest: "Makefile",
+					},
+				},
+			},
+			{
+				Path:         "build/soong",
+				Name:         "platform/build/soong",
+				GroupsString: "pdk,tradefed",
+				Groups: map[string]bool{
+					"pdk":      true,
+					"tradefed": true,
+				},
+				Linkfile: []Linkfile{
+					{
+						Src:  "root.bp",
+						Dest: "Android.bp",
+					},
+				},
+			},
+		},
+	}
+
+	if !reflect.DeepEqual(manifest, want) {
+		t.Errorf("got %v, want %v", manifest, want)
+	}
+}
+
+func TestRoundtrip(t *testing.T) {
+	manifest, err := Parse([]byte(aospManifest))
+	if err != nil {
+		t.Fatalf("Parse: %v", err)
+	}
+
+	xml, err := manifest.MarshalXML()
+	if err != nil {
+		t.Errorf("MarshalXML: %v", err)
+	}
+
+	roundtrip, err := Parse(xml)
+	if err != nil {
+		t.Errorf("Parse(roundtrip): %v", err)
+	}
+
+	if !reflect.DeepEqual(roundtrip, manifest) {
+		t.Errorf("got roundtrip %#v, want %#v", roundtrip, manifest)
+	}
+}
diff --git a/manifest/types.go b/manifest/types.go
new file mode 100644
index 0000000..343b42c
--- /dev/null
+++ b/manifest/types.go
@@ -0,0 +1,85 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed 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 manifest manipulates Manifest files as described at
+// https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.txt.
+package manifest
+
+// Copyfile indicates that a file should be copied in a checkout
+type Copyfile struct {
+	Src  string `xml:"src,attr"`
+	Dest string `xml:"dest,attr"`
+}
+
+// Linkfile indicates that a file should be symlinked in a checkout
+type Linkfile struct {
+	Src  string `xml:"src,attr"`
+	Dest string `xml:"dest,attr"`
+}
+
+// Project represents a single git repository that should be stitched
+// into the checkout.
+type Project struct {
+	Path         string          `xml:"path,attr"`
+	Name         string          `xml:"name,attr"`
+	Remote       string          `xml:"remote,attr,omitempty"`
+	Copyfile     []Copyfile      `xml:"copyfile,omitempty"`
+	Linkfile     []Linkfile      `xml:"linkfile,omitempty"`
+	GroupsString string          `xml:"groups,attr,omitempty"`
+	Groups       map[string]bool `xml:"-"`
+
+	Revision   string `xml:"revision,attr,omitempty"`
+	DestBranch string `xml:"dest-branch,attr,omitempty"`
+	SyncJ      string `xml:"sync-j,attr,omitempty"`
+	SyncC      string `xml:"sync-c,attr,omitempty"`
+	SyncS      string `xml:"sync-s,attr,omitempty"`
+
+	Upstream   string `xml:"upstream,attr,omitempty"`
+	CloneDepth string `xml:"clone-depth,attr,omitempty"`
+	ForcePath  string `xml:"force-path,attr,omitempty"`
+
+	// This is not part of the Manifest spec.
+	CloneURL string `xml:"clone-url,attr,omitempty"`
+}
+
+// Remote describes a host where a set of projects is hosted.
+type Remote struct {
+	Alias    string `xml:"alias,attr"`
+	Name     string `xml:"name,attr"`
+	Fetch    string `xml:"fetch,attr"`
+	Review   string `xml:"review,attr"`
+	Revision string `xml:"revision,attr"`
+}
+
+// Default holds default Project settings.
+type Default struct {
+	Revision   string `xml:"revision,attr"`
+	Remote     string `xml:"remote,attr"`
+	DestBranch string `xml:"dest-branch,attr"`
+	SyncJ      string `xml:"sync-j,attr"`
+	SyncC      string `xml:"sync-c,attr"`
+	SyncS      string `xml:"sync-s,attr"`
+}
+
+type ManifestServer struct {
+	URL string `xml:"url,attr"`
+}
+
+// Manifest holds the entire manifest, describing a set of git
+// projects to be stitched together
+type Manifest struct {
+	Default Default   `xml:"default"`
+	Remote  []Remote  `xml:"remote"`
+	Project []Project `xml:"project"`
+}