You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mynewt.apache.org by GitBox <gi...@apache.org> on 2018/05/08 17:58:55 UTC

[GitHub] ccollins476ad closed pull request #165: Allow git commit strings in `project.yml`

ccollins476ad closed pull request #165: Allow git commit strings in `project.yml`
URL: https://github.com/apache/mynewt-newt/pull/165
 
 
   

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/newt/deprepo/deprepo.go b/newt/deprepo/deprepo.go
index 7299c373..5d1069df 100644
--- a/newt/deprepo/deprepo.go
+++ b/newt/deprepo/deprepo.go
@@ -26,6 +26,17 @@ type Conflict struct {
 	Filters  []Filter
 }
 
+// Returns a sorted slice of all constituent repo names.
+func (vm VersionMap) SortedNames() []string {
+	names := make([]string, 0, len(vm))
+	for name, _ := range vm {
+		names = append(names, name)
+	}
+
+	sort.Strings(names)
+	return names
+}
+
 // Returns a slice of all constituent repos, sorted by name.
 func (rm RepoMap) Sorted() []*repo.Repo {
 	names := make([]string, 0, len(rm))
@@ -165,12 +176,12 @@ func PruneMatrix(m *Matrix, repos RepoMap, rootReqs RequirementMap) error {
 		row := m.FindRow(r.Name())
 		if row != nil && len(row.Vers) == 1 {
 			ver := row.Vers[0]
-			branch, err := r.BranchFromVer(ver)
+			commit, err := r.CommitFromVer(ver)
 			if err != nil {
 				return err
 			}
 			depRepo := repos[dep.Name]
-			for _, ddep := range depRepo.BranchDepMap()[branch] {
+			for _, ddep := range depRepo.CommitDepMap()[commit] {
 				name := fmt.Sprintf("%s,%s", depRepo.Name(), ver.String())
 				if err := recurse(name, ddep); err != nil {
 					return err
diff --git a/newt/deprepo/graph.go b/newt/deprepo/graph.go
index 648c874d..dbf04943 100644
--- a/newt/deprepo/graph.go
+++ b/newt/deprepo/graph.go
@@ -50,14 +50,18 @@ type RevdepGraphNode struct {
 // Value: The corresponding list of dependencies.
 type RevdepGraph map[string][]RevdepGraphNode
 
-func (dep *Dependent) String() string {
-	if dep.Name == rootDependencyName {
+func repoNameVerString(repoName string, ver newtutil.RepoVersion) string {
+	if repoName == rootDependencyName {
 		return "project.yml"
 	} else {
-		return fmt.Sprintf("%s-%s", dep.Name, dep.Ver.String())
+		return fmt.Sprintf("%s-%s", repoName, ver.String())
 	}
 }
 
+func (dep *Dependent) String() string {
+	return repoNameVerString(dep.Name, dep.Ver)
+}
+
 func (dgn *DepGraphNode) String() string {
 	return fmt.Sprintf("%s,%s", dgn.Name,
 		newtutil.RepoVerReqsString(dgn.VerReqs))
@@ -69,7 +73,28 @@ func (dg DepGraph) String() string {
 	for dependent, nodes := range dg {
 		line := fmt.Sprintf("%s:", dependent.String())
 		for _, node := range nodes {
-			line += fmt.Sprintf(" %s", node.String())
+			line += fmt.Sprintf(" (%s)", node.String())
+		}
+
+		lines = append(lines, line)
+	}
+
+	sort.Strings(lines)
+	return strings.Join(lines, "\n")
+}
+
+func (rgn *RevdepGraphNode) String() string {
+	return fmt.Sprintf("%s,%s", repoNameVerString(rgn.Name, rgn.Ver),
+		newtutil.RepoVerReqsString(rgn.VerReqs))
+}
+
+func (rg RevdepGraph) String() string {
+	lines := make([]string, 0, len(rg))
+
+	for repoName, nodes := range rg {
+		line := fmt.Sprintf("%s:", repoName)
+		for _, node := range nodes {
+			line += fmt.Sprintf(" (%s)", node.String())
 		}
 
 		lines = append(lines, line)
diff --git a/newt/downloader/downloader.go b/newt/downloader/downloader.go
index adb8e7ad..af3458f8 100644
--- a/newt/downloader/downloader.go
+++ b/newt/downloader/downloader.go
@@ -25,29 +25,38 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"sort"
 	"strings"
 
 	log "github.com/Sirupsen/logrus"
 
-	"mynewt.apache.org/newt/newt/newtutil"
 	"mynewt.apache.org/newt/newt/settings"
 	"mynewt.apache.org/newt/util"
 )
 
+type DownloaderCommitType int
+
+const (
+	COMMIT_TYPE_REMOTE_BRANCH DownloaderCommitType = iota
+	COMMIT_TYPE_LOCAL_BRANCH
+	COMMIT_TYPE_TAG
+	COMMIT_TYPE_HASH
+)
+
 type Downloader interface {
 	FetchFile(path string, filename string, dstDir string) error
-	Branch() string
-	SetBranch(branch string)
+	GetCommit() string
+	SetCommit(commit string)
 	DownloadRepo(commit string, dstPath string) error
-	CurrentBranch(path string) (string, error)
+	HashFor(path string, commit string) (string, error)
+	CommitsFor(path string, commit string) ([]string, error)
 	UpdateRepo(path string, branchName string) error
-	CleanupRepo(path string, branchName string) error
-	LocalDiff(path string) ([]byte, error)
 	AreChanges(path string) (bool, error)
+	CommitType(path string, commit string) (DownloaderCommitType, error)
 }
 
 type GenericDownloader struct {
-	branch string
+	commit string
 
 	// Whether 'origin' has been fetched during this run.
 	fetched bool
@@ -119,18 +128,12 @@ func executeGitCommand(dir string, cmd []string, logCmd bool) ([]byte, error) {
 	return output, nil
 }
 
-func isTag(repoDir string, branchName string) bool {
-	cmd := []string{"tag", "--list"}
-	output, _ := executeGitCommand(repoDir, cmd, true)
-	return strings.Contains(string(output), branchName)
-}
-
-func branchExists(repoDir string, branchName string) bool {
+func commitExists(repoDir string, commit string) bool {
 	cmd := []string{
 		"show-ref",
 		"--verify",
 		"--quiet",
-		"refs/heads/" + branchName,
+		"refs/heads/" + commit,
 	}
 	_, err := executeGitCommand(repoDir, cmd, true)
 	return err == nil
@@ -169,18 +172,27 @@ func updateSubmodules(path string) error {
 // handling and result in dettached from HEAD state.
 func checkout(repoDir string, commit string) error {
 	var cmd []string
-	if isTag(repoDir, commit) && !branchExists(repoDir, commit) {
+	ct, err := commitType(repoDir, commit)
+	if err != nil {
+		return err
+	}
+
+	full, err := fullCommitName(repoDir, commit)
+	if err != nil {
+		return err
+	}
+
+	if ct == COMMIT_TYPE_TAG {
 		util.StatusMessage(util.VERBOSITY_VERBOSE, "Will create new branch %s"+
-			" from tag %s\n", commit, "tags/"+commit)
+			" from %s\n", commit, full)
 		cmd = []string{
 			"checkout",
-			"tags/" + commit,
+			full,
 			"-b",
 			commit,
 		}
 	} else {
-		util.StatusMessage(util.VERBOSITY_VERBOSE, "Will checkout branch %s\n",
-			commit)
+		util.StatusMessage(util.VERBOSITY_VERBOSE, "Will checkout %s\n", full)
 		cmd = []string{
 			"checkout",
 			commit,
@@ -192,7 +204,7 @@ func checkout(repoDir string, commit string) error {
 
 	// Always initialize and update submodules on checkout.  This prevents the
 	// repo from being in a modified "(new commits)" state immediately after
-	// switching branches.  If the submodules have already been updated, this
+	// switching commits.  If the submodules have already been updated, this
 	// does not generate any network activity.
 	if err := initSubmodules(repoDir); err != nil {
 		return err
@@ -204,51 +216,89 @@ func checkout(repoDir string, commit string) error {
 	return nil
 }
 
-// mergeBranches applies upstream changes to the local copy and must be
+// mergees applies upstream changes to the local copy and must be
 // preceeded by a "fetch" to achieve any meaningful result.
-func mergeBranch(repoDir string, branch string) error {
-	if err := checkout(repoDir, branch); err != nil {
+func merge(repoDir string, commit string) error {
+	if err := checkout(repoDir, commit); err != nil {
+		return err
+	}
+
+	ct, err := commitType(repoDir, commit)
+	if err != nil {
+		return err
+	}
+
+	// We want to merge the remote version of this branch.
+	if ct == COMMIT_TYPE_LOCAL_BRANCH {
+		ct = COMMIT_TYPE_REMOTE_BRANCH
+	}
+
+	full, err := prependCommitPrefix(commit, ct)
+	if err != nil {
 		return err
 	}
 
-	fullName := "origin/" + branch
 	if _, err := executeGitCommand(
-		repoDir, []string{"merge", fullName}, true); err != nil {
+		repoDir, []string{"merge", full}, true); err != nil {
 
 		util.StatusMessage(util.VERBOSITY_VERBOSE,
-			"Merging changes from %s: %s\n", fullName, err)
+			"Merging changes from %s: %s\n", full, err)
 		return err
 	}
 
 	util.StatusMessage(util.VERBOSITY_VERBOSE,
-		"Merging changes from %s\n", fullName)
+		"Merging changes from %s\n", full)
 	return nil
 }
 
-// stash saves current changes locally and returns if a new stash was
-// created (if there where no changes, there's no need to stash)
-func stash(repoDir string) (bool, error) {
-	util.StatusMessage(util.VERBOSITY_VERBOSE, "Stashing local changes\n")
-	output, err := executeGitCommand(repoDir, []string{"stash"}, true)
+func mergeBase(repoDir string, commit string) (string, error) {
+	cmd := []string{
+		"merge-base",
+		commit,
+		commit,
+	}
+	o, err := executeGitCommand(repoDir, cmd, true)
 	if err != nil {
-		return false, err
+		return "", err
 	}
-	return strings.Contains(string(output), "Saved"), nil
-}
 
-func stashPop(repoDir string) error {
-	util.StatusMessage(util.VERBOSITY_VERBOSE, "Un-stashing local changes\n")
-	_, err := executeGitCommand(repoDir, []string{"stash", "pop"}, true)
-	return err
+	return strings.TrimSpace(string(o)), nil
 }
 
-func clean(repoDir string) error {
-	_, err := executeGitCommand(repoDir, []string{"clean", "-f"}, true)
-	return err
+func branchExists(repoDir string, branchName string) bool {
+	cmd := []string{
+		"show-ref",
+		"--verify",
+		"--quiet",
+		"refs/heads/" + branchName,
+	}
+	_, err := executeGitCommand(repoDir, cmd, true)
+	return err == nil
 }
 
-func diff(repoDir string) ([]byte, error) {
-	return executeGitCommand(repoDir, []string{"diff"}, true)
+func commitType(repoDir string, commit string) (DownloaderCommitType, error) {
+	if commit == "HEAD" {
+		return COMMIT_TYPE_HASH, nil
+	}
+
+	if _, err := mergeBase(repoDir, commit); err == nil {
+		// Distinguish local branch from hash.
+		if branchExists(repoDir, commit) {
+			return COMMIT_TYPE_LOCAL_BRANCH, nil
+		} else {
+			return COMMIT_TYPE_HASH, nil
+		}
+	}
+
+	if _, err := mergeBase(repoDir, "origin/"+commit); err == nil {
+		return COMMIT_TYPE_REMOTE_BRANCH, nil
+	}
+	if _, err := mergeBase(repoDir, "tags/"+commit); err == nil {
+		return COMMIT_TYPE_TAG, nil
+	}
+
+	return DownloaderCommitType(-1), util.FmtNewtError(
+		"Cannot determine commit type of \"%s\"", commit)
 }
 
 func areChanges(repoDir string) (bool, error) {
@@ -265,6 +315,28 @@ func areChanges(repoDir string) (bool, error) {
 	return len(o) > 0, nil
 }
 
+func prependCommitPrefix(commit string, ct DownloaderCommitType) (string, error) {
+	switch ct {
+	case COMMIT_TYPE_REMOTE_BRANCH:
+		return "origin/" + commit, nil
+	case COMMIT_TYPE_TAG:
+		return "tags/" + commit, nil
+	case COMMIT_TYPE_HASH, COMMIT_TYPE_LOCAL_BRANCH:
+		return commit, nil
+	default:
+		return "", util.FmtNewtError("unknown commit type: %d", int(ct))
+	}
+}
+
+func fullCommitName(path string, commit string) (string, error) {
+	ct, err := commitType(path, commit)
+	if err != nil {
+		return "", err
+	}
+
+	return prependCommitPrefix(commit, ct)
+}
+
 func showFile(
 	path string, branch string, filename string, dstDir string) error {
 
@@ -272,9 +344,14 @@ func showFile(
 		return util.ChildNewtError(err)
 	}
 
+	full, err := fullCommitName(path, branch)
+	if err != nil {
+		return err
+	}
+
 	cmd := []string{
 		"show",
-		fmt.Sprintf("origin/%s:%s", branch, filename),
+		fmt.Sprintf("%s:%s", full, filename),
 	}
 
 	dstPath := fmt.Sprintf("%s/%s", dstDir, filename)
@@ -291,12 +368,63 @@ func showFile(
 	return nil
 }
 
-func (gd *GenericDownloader) Branch() string {
-	return gd.branch
+func (gd *GenericDownloader) GetCommit() string {
+	return gd.commit
+}
+
+func (gd *GenericDownloader) SetCommit(branch string) {
+	gd.commit = branch
 }
 
-func (gd *GenericDownloader) SetBranch(branch string) {
-	gd.branch = branch
+func (gd *GenericDownloader) CommitType(
+	path string, commit string) (DownloaderCommitType, error) {
+
+	return commitType(path, commit)
+}
+
+func (gd *GenericDownloader) HashFor(path string, commit string) (string, error) {
+	full, err := fullCommitName(path, commit)
+	if err != nil {
+		return "", err
+	}
+	cmd := []string{"rev-parse", full}
+	o, err := executeGitCommand(path, cmd, true)
+	if err != nil {
+		return "", err
+	}
+
+	return strings.TrimSpace(string(o)), nil
+}
+
+func (gd *GenericDownloader) CommitsFor(
+	path string, commit string) ([]string, error) {
+
+	// Hash.
+	hash, err := gd.HashFor(path, commit)
+	if err != nil {
+		return nil, err
+	}
+
+	// Branches and tags.
+	cmd := []string{
+		"for-each-ref",
+		"--format=%(refname:short)",
+		"--points-at",
+		hash,
+	}
+	o, err := executeGitCommand(path, cmd, true)
+	if err != nil {
+		return nil, err
+	}
+
+	lines := []string{hash}
+	text := strings.TrimSpace(string(o))
+	if text != "" {
+		lines = append(lines, strings.Split(text, "\n")...)
+	}
+
+	sort.Strings(lines)
+	return lines, nil
 }
 
 // Fetches the downloader's origin remote if it hasn't been fetched yet during
@@ -352,65 +480,30 @@ func (gd *GithubDownloader) FetchFile(
 		return err
 	}
 
-	if err := showFile(path, gd.Branch(), filename, dstDir); err != nil {
+	if err := showFile(path, gd.GetCommit(), filename, dstDir); err != nil {
 		return err
 	}
 
 	return nil
 }
 
-func (gd *GithubDownloader) CurrentBranch(path string) (string, error) {
-	cmd := []string{"rev-parse", "--abbrev-ref", "HEAD"}
-	branch, err := executeGitCommand(path, cmd, true)
-	return strings.Trim(string(branch), "\r\n"), err
-}
-
 func (gd *GithubDownloader) UpdateRepo(path string, branchName string) error {
 	err := gd.fetch(path)
 	if err != nil {
 		return err
 	}
 
-	stashed, err := stash(path)
-	if err != nil {
-		return err
-	}
-
 	// Ignore error, probably resulting from a branch not available at origin
 	// anymore.
-	mergeBranch(path, branchName)
+	merge(path, branchName)
 
 	if err := checkout(path, branchName); err != nil {
 		return err
 	}
 
-	if stashed {
-		return stashPop(path)
-	}
-
 	return nil
 }
 
-func (gd *GithubDownloader) CleanupRepo(path string, branchName string) error {
-	_, err := stash(path)
-	if err != nil {
-		return err
-	}
-
-	err = clean(path)
-	if err != nil {
-		return err
-	}
-
-	// TODO: needs handling of non-tracked files
-
-	return gd.UpdateRepo(path, branchName)
-}
-
-func (gd *GithubDownloader) LocalDiff(path string) ([]byte, error) {
-	return diff(path)
-}
-
 func (gd *GithubDownloader) AreChanges(path string) (bool, error) {
 	return areChanges(path)
 }
@@ -539,66 +632,30 @@ func (gd *GitDownloader) FetchFile(
 		return err
 	}
 
-	if err := showFile(path, gd.Branch(), filename, dstDir); err != nil {
+	if err := showFile(path, gd.GetCommit(), filename, dstDir); err != nil {
 		return err
 	}
 
 	return nil
 }
 
-func (gd *GitDownloader) CurrentBranch(path string) (string, error) {
-	cmd := []string{"rev-parse", "--abbrev-ref", "HEAD"}
-	branch, err := executeGitCommand(path, cmd, true)
-	return strings.Trim(string(branch), "\r\n"), err
-}
-
 func (gd *GitDownloader) UpdateRepo(path string, branchName string) error {
 	err := gd.fetch(path)
 	if err != nil {
 		return err
 	}
 
-	stashed, err := stash(path)
-	if err != nil {
-		return err
-	}
-
 	// Ignore error, probably resulting from a branch not available at origin
 	// anymore.
-	mergeBranch(path, branchName)
+	merge(path, branchName)
 
-	err = checkout(path, branchName)
-	if err != nil {
+	if err := checkout(path, branchName); err != nil {
 		return err
 	}
 
-	if stashed {
-		return stashPop(path)
-	}
-
 	return nil
 }
 
-func (gd *GitDownloader) CleanupRepo(path string, branchName string) error {
-	_, err := stash(path)
-	if err != nil {
-		return err
-	}
-
-	err = clean(path)
-	if err != nil {
-		return err
-	}
-
-	// TODO: needs handling of non-tracked files
-
-	return gd.UpdateRepo(path, branchName)
-}
-
-func (gd *GitDownloader) LocalDiff(path string) ([]byte, error) {
-	return diff(path)
-}
-
 func (gd *GitDownloader) AreChanges(path string) (bool, error) {
 	return areChanges(path)
 }
@@ -660,33 +717,11 @@ func (ld *LocalDownloader) FetchFile(
 	return nil
 }
 
-func (ld *LocalDownloader) CurrentBranch(path string) (string, error) {
-	cmd := []string{"rev-parse", "--abbrev-ref", "HEAD"}
-	branch, err := executeGitCommand(path, cmd, true)
-	return strings.Trim(string(branch), "\r\n"), err
-}
-
 func (ld *LocalDownloader) UpdateRepo(path string, branchName string) error {
 	os.RemoveAll(path)
 	return ld.DownloadRepo(branchName, path)
 }
 
-func (ld *LocalDownloader) CleanupRepo(path string, branchName string) error {
-	os.RemoveAll(path)
-
-	tmpdir, err := newtutil.MakeTempRepoDir()
-	if err != nil {
-		return err
-	}
-	defer os.RemoveAll(tmpdir)
-
-	return ld.DownloadRepo(branchName, tmpdir)
-}
-
-func (ld *LocalDownloader) LocalDiff(path string) ([]byte, error) {
-	return diff(path)
-}
-
 func (ld *LocalDownloader) AreChanges(path string) (bool, error) {
 	return areChanges(path)
 }
diff --git a/newt/install/install.go b/newt/install/install.go
new file mode 100644
index 00000000..4f4eb666
--- /dev/null
+++ b/newt/install/install.go
@@ -0,0 +1,844 @@
+// 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.
+
+// ----------------------------------------------------------------------------
+// install: Handles project installs, upgrades, and syncs.
+// ----------------------------------------------------------------------------
+//
+// This file implements three newt operations:
+// * Install - Downloads repos that aren't installed yet.  The downloaded
+//             version matches what `project.yml` specifies.
+//
+// * Upgrade - Ensures the installed version of each repo matches what
+//             `project.yml` specifies.  This is similar to Install, but it
+//             also operates on already-installed repos.
+//
+// * Sync    - Fetches and pulls the latest for each repo, but does not change
+//             the branch (version).
+//
+// All three operations operate on the repos specified in the `project.yml`
+// file and the set of each repo's dependencies.
+//
+// Within the `project.yml` file, repo requirements are expressed with one of
+// the following forms:
+//     * [Normalized version]: #.#.#
+//           (e.g., "1.3.0")
+//     * [Floating version]:   #[.#]-<stability
+//           (e.g., "0-dev")
+//     * [Git commit]:         <git-commit-ish>-commit
+//           (e.g., "0aae710654b48d9a84d54de771cc18427709df7d-commit")
+//
+// The first two types (normalized version and floating version) are called
+// "version specifiers".  Version specifiers map to "official releases", while
+// git commits typically map to "custom versions".
+//
+// Before newt can do anything with a repo requirement, it needs to extrapolate
+// two pieces of information:
+// 1. The normalized version number.
+// 2. The git commit.
+//
+// Newt needs the normalized version to determine the repo's dependencies, and
+// to ensure the version of the repo is compatible with the version of newt
+// being used.  Newt needs the git commit so that it knows how to checkout the
+// desired repo version.
+//
+// ### VERSION SPECIFIERS
+//
+// A repo's `repository.yml` file maps version specifiers to git commits in its
+// `repo.versions` field.  For example:
+//    repo.versions:
+//        "0.0.0": "master"
+//        "1.0.0": "mynewt_1_0_0_tag"
+//        "1.1.0": "mynewt_1_1_0_tag"
+//        "0-dev": "0.0.0"
+//
+// By performing a series of recursive lookups, newt converts a version
+// specifier to a normalized-version,git-commit pair.
+//
+// ### GIT COMMITS
+//
+// When newt encounters a git commit in the `project.yml` file, it already has
+// one piece of information that it needs: the git commit.  Newt uses the
+// following procedure to extrapolate its corresponding repo version:
+//
+// 1. If the repo at the commit contains a `version.yml` file, read the version
+//    from this file.
+// 2. Else, if the repo's `repository.yml` file maps the commit to a version
+//    number, use that version number.
+// 3. Else, warn the user and assume 0.0.0.
+//
+// The `version.yml` file is expected to be present in every commit in a repo.
+// It has the following form:
+//    repo.version: <normalized-version-number>
+//
+// For example, if commit 10 of repo X contains the following `version.yml`
+// file:
+//    repo.version: 1.10.0
+//
+// and commit 20 of repo X changes `version.yml` to:
+//    repo.version: 2.0.0
+//
+// then newt extrapolates 1.10.0 from commits 10 through 19 (inclusive).
+// Commit 20 and beyond correspond to 2.0.0.
+//
+// ### VERSION STRINGS
+//
+// Newt uses the following procedure when displaying a repo version to the
+// user:
+//
+// Official releases are expressed as a normalized version.
+//     e.g., 1.10.0
+//
+// Custom versions are expressed with the following form:
+//     <extrapolated-version>/<git-commit>
+//
+// E.g.,:
+//     0.0.0/0aae710654b48d9a84d54de771cc18427709df7d
+// ----------------------------------------------------------------------------
+
+package install
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"sort"
+	"strings"
+
+	log "github.com/Sirupsen/logrus"
+
+	"mynewt.apache.org/newt/newt/deprepo"
+	"mynewt.apache.org/newt/newt/newtutil"
+	"mynewt.apache.org/newt/newt/repo"
+	"mynewt.apache.org/newt/util"
+)
+
+type installOp int
+
+const (
+	INSTALL_OP_INSTALL installOp = iota
+	INSTALL_OP_UPGRADE
+	INSTALL_OP_SYNC
+)
+
+// Determines the currently installed version of the specified repo.  If the
+// repo doesn't have a valid `version.yml` file, and it isn't using a commit
+// that maps to a version, 0.0.0 is returned.
+func detectVersion(r *repo.Repo) (newtutil.RepoVersion, error) {
+	ver, err := r.InstalledVersion()
+	if err != nil {
+		return newtutil.RepoVersion{}, err
+	}
+
+	// Fallback to 0.0.0 if version detection failed.
+	if ver == nil {
+		commit, err := r.CurrentHash()
+		if err != nil {
+			return newtutil.RepoVersion{}, err
+		}
+
+		// Create a 0.0.0 version specifier with the indicated commit string.
+		ver = &newtutil.RepoVersion{
+			Commit: commit,
+		}
+
+		util.StatusMessage(util.VERBOSITY_QUIET,
+			"WARNING: Could not detect version of installed repo \"%s\"; "+
+				"assuming %s\n", r.Name(), ver.String())
+	}
+
+	log.Debugf("currently installed version of repo \"%s\": %s",
+		r.Name(), ver.String())
+
+	return *ver, nil
+}
+
+type Installer struct {
+	// Map of all repos in the project.
+	repos deprepo.RepoMap
+
+	// Version of each installed repo.
+	vers deprepo.VersionMap
+
+	// Required versions of installed repos, as read from `project.yml`.
+	reqs deprepo.RequirementMap
+}
+
+func NewInstaller(repos deprepo.RepoMap,
+	reqs deprepo.RequirementMap) (Installer, error) {
+
+	inst := Installer{
+		repos: repos,
+		vers:  deprepo.VersionMap{},
+		reqs:  reqs,
+	}
+
+	// Detect the installed versions of all repos.
+	for n, r := range inst.repos {
+		if !r.IsLocal() && !r.IsNewlyCloned() {
+			ver, err := detectVersion(r)
+			if err != nil {
+				return inst, err
+			}
+
+			inst.vers[n] = ver
+		}
+	}
+
+	return inst, nil
+}
+
+// Retrieves the installed version of the specified repo.  Versions get
+// detected and cached when the installer is constructed.  This function just
+// retrieves the corresponding entry from the cache.
+func (inst *Installer) installedVer(repoName string) *newtutil.RepoVersion {
+	ver, ok := inst.vers[repoName]
+	if !ok {
+		return nil
+	} else {
+		return &ver
+	}
+}
+
+// Given a slice of repos, recursively appends all depended-on repos, ensuring
+// each element is unique.
+//
+// @param repos                 The list of dependent repos to process.
+// @param vm                    Indicates the version of each repo to consider.
+//                                  Pass nil to consider all versions of all
+//                                  repos.
+//
+// @return []*repo.Repo         The original list, augmented with all
+//                                  depended-on repos.
+func (inst *Installer) ensureDepsInList(repos []*repo.Repo,
+	vm deprepo.VersionMap) []*repo.Repo {
+
+	seen := map[string]struct{}{}
+
+	var recurse func(r *repo.Repo) []*repo.Repo
+	recurse = func(r *repo.Repo) []*repo.Repo {
+		// Don't process this repo a second time.
+		if _, ok := seen[r.Name()]; ok {
+			return nil
+		}
+		seen[r.Name()] = struct{}{}
+
+		result := []*repo.Repo{r}
+
+		var deps []*repo.RepoDependency
+		if vm == nil {
+			deps = r.AllDeps()
+		} else {
+			deps = r.DepsForVersion(vm[r.Name()])
+		}
+		for _, d := range deps {
+			depRepo := inst.repos[d.Name]
+			result = append(result, recurse(depRepo)...)
+		}
+
+		return result
+	}
+
+	deps := []*repo.Repo{}
+	for _, r := range repos {
+		deps = append(deps, recurse(r)...)
+	}
+
+	return deps
+}
+
+// Normalizes the installer's set of repo requirements.  Only the repos in the
+// specified slice are considered.
+//
+// A repo requirement takes one of two forms:
+//  * Version specifier (e.g., 1.3.0. or 0-dev).
+//  * Git commit (e.g., 1f48a3c or master).
+//
+// This function converts requirements from the second form to the first.  A
+// git commit is converted to a version number with this procedure:
+//
+// 1. If the specified commit contains a `version.yml` file, read the version
+//    from this file.
+// 2. Else, if the repo's `repository.yml` file maps the commit to a version
+//    number, use that version number.
+// 3. Else, assume 0.0.0.
+func (inst *Installer) inferReqVers(repos []*repo.Repo) error {
+	for _, r := range repos {
+		reqs, ok := inst.reqs[r.Name()]
+		if ok {
+			for i, req := range reqs {
+				if req.Ver.Commit != "" {
+					ver, err := r.NonInstalledVersion(req.Ver.Commit)
+					if err != nil {
+						return err
+					}
+
+					if ver == nil {
+						util.StatusMessage(util.VERBOSITY_QUIET,
+							"WARNING: Could not detect version of "+
+								"requested repo %s:%s; assuming 0.0.0\n",
+							r.Name(), req.Ver.Commit)
+
+						ver = &req.Ver
+					}
+					reqs[i].Ver = *ver
+					reqs[i].Ver.Commit, err = r.HashFromVer(reqs[i].Ver)
+					if err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// Determines if the `project.yml` file specifies a nonexistent repo version.
+// Only the repos in the specified slice are considered.
+//
+// @param repos                 The list of repos to consider during the check.
+// @param m                     A matrix containing all versions of the
+//                                  specified repos.
+//
+// @return error                Error if any repo requirement is invalid.
+func (inst *Installer) detectIllegalRepoReqs(
+	repos []*repo.Repo, m deprepo.Matrix) error {
+
+	var lines []string
+	for _, r := range repos {
+		reqs, ok := inst.reqs[r.Name()]
+		if ok {
+			row := m.FindRow(r.Name())
+			if row == nil {
+				return util.FmtNewtError(
+					"internal error; repo \"%s\" missing from matrix", r.Name())
+			}
+
+			r := inst.repos[r.Name()]
+			nreqs, err := r.NormalizeVerReqs(reqs)
+			if err != nil {
+				return err
+			}
+
+			anySatisfied := false
+			for _, ver := range row.Vers {
+				if ver.SatisfiesAll(nreqs) {
+					anySatisfied = true
+					break
+				}
+			}
+			if !anySatisfied {
+				line := fmt.Sprintf("    %s,%s", r.Name(),
+					newtutil.RepoVerReqsString(nreqs))
+				lines = append(lines, line)
+			}
+		}
+	}
+
+	if len(lines) > 0 {
+		sort.Strings(lines)
+		return util.NewNewtError(
+			"project.yml file specifies nonexistent repo versions:\n" +
+				strings.Join(lines, "\n"))
+	}
+
+	return nil
+}
+
+// Removes repos that shouldn't be installed from the specified list.  A repo
+// should not be installed if it is already installed (any version).
+//
+// @param repos                 The list of repos to filter.
+//
+// @return []*Repo              The filtered list of repos.
+func (inst *Installer) filterInstallList(
+	vm deprepo.VersionMap) (deprepo.VersionMap, error) {
+
+	filtered := deprepo.VersionMap{}
+
+	for name, ver := range vm {
+		curVer := inst.installedVer(name)
+		if curVer == nil {
+			filtered[name] = ver
+		} else {
+			util.StatusMessage(util.VERBOSITY_DEFAULT,
+				"Skipping \"%s\": already installed (%s)\n",
+				name, curVer.String())
+		}
+	}
+
+	return filtered, nil
+}
+
+// Indicates whether a repo should be upgraded to the specified version.  A
+// repo should be upgraded if it is not currently installed, or if a version
+// other than the desired one is installed.
+func (inst *Installer) shouldUpgradeRepo(
+	repoName string, destVer newtutil.RepoVersion) (bool, error) {
+
+	curVer := inst.installedVer(repoName)
+
+	// If the repo isn't installed, it needs to be upgraded.
+	if curVer == nil {
+		return true, nil
+	}
+
+	r := inst.repos[repoName]
+	if r == nil {
+		return false, util.FmtNewtError(
+			"internal error: nonexistent repo has version: %s", repoName)
+	}
+
+	if !r.VersionsEqual(*curVer, destVer) {
+		return true, nil
+	}
+
+	equiv, err := r.CommitsEquivalent(curVer.Commit, destVer.Commit)
+	if err != nil {
+		return false, err
+	}
+
+	return !equiv, nil
+}
+
+// Removes repos that shouldn't be upgraded from the specified list.  A repo
+// should not be upgraded if the desired version is already installed.
+//
+// @param repos                 The list of repos to filter.
+// @param vm                    Specifies the desired version of each repo.
+//
+// @return []*Repo              The filtered list of repos.
+func (inst *Installer) filterUpgradeList(
+	vm deprepo.VersionMap) (deprepo.VersionMap, error) {
+
+	filtered := deprepo.VersionMap{}
+
+	for name, ver := range vm {
+		doUpgrade, err := inst.shouldUpgradeRepo(name, ver)
+		if err != nil {
+			return nil, err
+		}
+		if doUpgrade {
+			filtered[name] = ver
+		} else {
+			curVer := inst.installedVer(name)
+			if curVer == nil {
+				return nil, util.FmtNewtError(
+					"internal error: should upgrade repo %s, "+
+						"but no version installed",
+					name)
+			}
+			curVer.Commit = ver.Commit
+			util.StatusMessage(util.VERBOSITY_DEFAULT,
+				"Skipping \"%s\": already upgraded (%s)\n",
+				name, curVer.String())
+		}
+	}
+
+	return filtered, nil
+}
+
+// Describes an imminent install or upgrade operation to the user.  The
+// displayed message applies to the specified repo.
+func (inst *Installer) installMessageOneRepo(
+	repoName string, op installOp, force bool, curVer *newtutil.RepoVersion,
+	destVer newtutil.RepoVersion) (string, error) {
+
+	// If the repo isn't installed yet, this is an install, not an upgrade.
+	if op == INSTALL_OP_UPGRADE && curVer == nil {
+		op = INSTALL_OP_INSTALL
+	}
+
+	var verb string
+	switch op {
+	case INSTALL_OP_INSTALL:
+		if !force {
+			verb = "install"
+		} else {
+			verb = "reinstall"
+		}
+
+	case INSTALL_OP_UPGRADE:
+		verb = "upgrade"
+
+	case INSTALL_OP_SYNC:
+		verb = "sync"
+
+	default:
+		return "", util.FmtNewtError(
+			"internal error: invalid install op: %v", op)
+	}
+
+	msg := fmt.Sprintf("    %s %s ", verb, repoName)
+	if op == INSTALL_OP_UPGRADE {
+		msg += fmt.Sprintf("(%s --> %s)", curVer.String(), destVer.String())
+	} else {
+		msg += fmt.Sprintf("(%s)", destVer.String())
+	}
+
+	return msg, nil
+}
+
+// Describes an imminent repo operation to the user.  In addition, prompts the
+// user for confirmation if the `-a` (ask) option was specified.
+func (inst *Installer) installPrompt(vm deprepo.VersionMap, op installOp,
+	force bool, ask bool) (bool, error) {
+
+	if len(vm) == 0 {
+		return true, nil
+	}
+
+	util.StatusMessage(util.VERBOSITY_DEFAULT,
+		"Making the following changes to the project:\n")
+
+	names := vm.SortedNames()
+	for _, name := range names {
+		r := inst.repos[name]
+		curVer := inst.installedVer(name)
+		if curVer != nil && curVer.Commit != "" {
+			c, err := r.CurrentHash()
+			if err == nil {
+				curVer.Commit = c
+			}
+		}
+		destVer := vm[name]
+
+		msg, err := inst.installMessageOneRepo(
+			name, op, force, curVer, destVer)
+		if err != nil {
+			return false, err
+		}
+
+		util.StatusMessage(util.VERBOSITY_DEFAULT, "%s\n", msg)
+	}
+
+	if !ask {
+		return true, nil
+	}
+
+	for {
+		line, more, err := bufio.NewReader(os.Stdin).ReadLine()
+		if more || err != nil {
+			return false, util.ChildNewtError(err)
+		}
+
+		trimmed := strings.ToLower(strings.TrimSpace(string(line)))
+		if len(trimmed) == 0 || strings.HasPrefix(trimmed, "y") {
+			// User wants to proceed.
+			return true, nil
+		}
+
+		if strings.HasPrefix(trimmed, "n") {
+			// User wants to cancel.
+			return false, nil
+		}
+
+		// Invalid response.
+		fmt.Printf("Invalid response.\n")
+	}
+}
+
+// Determines whether a repo version's `Commit` field should be maintained.  If
+// the commit corresponds exactly to a repo version in `repository.yml` (as
+// opposed to simply indicating its version in a `version.yml` file), then the
+// commit string should be discarded.  If the commit string is kept, newt
+// interprets the version as being different from the official release version,
+// triggering an upgrade.
+func (inst *Installer) shouldKeepCommit(
+	repoName string, commit string) (bool, error) {
+
+	if commit == "" {
+		return false, nil
+	}
+
+	r := inst.repos[repoName]
+	if r == nil {
+		return false, nil
+	}
+
+	vers, err := r.VersFromEquivCommit(commit)
+	if err != nil {
+		return false, err
+	}
+	if len(vers) > 0 {
+		return false, nil
+	}
+
+	return true, nil
+}
+
+// Filters out repos from a version map, keeping only those which are present
+// in the supplied slice.
+func filterVersionMap(
+	vm deprepo.VersionMap, keep []*repo.Repo) deprepo.VersionMap {
+
+	filtered := deprepo.VersionMap{}
+	for _, r := range keep {
+		name := r.Name()
+		if ver, ok := vm[name]; ok {
+			filtered[name] = ver
+		}
+	}
+
+	return filtered
+}
+
+// Creates a slice of repos, each corresponding to an element in the provided
+// version map.  The returned slice is sorted by repo name.
+func (inst *Installer) versionMapRepos(
+	vm deprepo.VersionMap) ([]*repo.Repo, error) {
+
+	repos := make([]*repo.Repo, 0, len(vm))
+
+	names := vm.SortedNames()
+	for _, name := range names {
+		r := inst.repos[name]
+		if r == nil {
+			return nil, util.FmtNewtError(
+				"internal error: repo \"%s\" missing from Installer#repos",
+				name)
+		}
+
+		repos = append(repos, r)
+	}
+
+	return repos, nil
+}
+
+// Calculates a map of repos and version numbers that should be included in an
+// install or upgrade operation.
+func (inst *Installer) calcVersionMap(candidates []*repo.Repo) (
+	deprepo.VersionMap, error) {
+
+	// Repos that depend on any specified repos must also be considered during
+	// the install / upgrade operation.
+	repoList := inst.ensureDepsInList(candidates, nil)
+
+	m, err := deprepo.BuildMatrix(repoList, inst.vers)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := inst.inferReqVers(repoList); err != nil {
+		return nil, err
+	}
+
+	// If the `project.yml` file specifies an invalid repo version, abort now.
+	if err := inst.detectIllegalRepoReqs(repoList, m); err != nil {
+		return nil, err
+	}
+
+	// Remove blocked repo versions from the table.
+	if err := deprepo.PruneMatrix(
+		&m, inst.repos, inst.reqs); err != nil {
+
+		return nil, err
+	}
+
+	// Construct a repo dependency graph from the `project.yml` version
+	// requirements and from each repo's dependency list.
+	dg, err := deprepo.BuildDepGraph(inst.repos, inst.reqs)
+	if err != nil {
+		return nil, err
+	}
+
+	log.Debugf("Repo dependency graph:\n%s\n", dg.String())
+	log.Debugf("Repo reverse dependency graph:\n%s\n", dg.Reverse().String())
+
+	// Try to find a version set that satisfies the dependency graph.  If no
+	// such set exists, report the conflicts and abort.
+	vm, conflicts := deprepo.FindAcceptableVersions(m, dg)
+	if vm == nil {
+		return nil, deprepo.ConflictError(conflicts)
+	}
+
+	log.Debugf("Repo version map:\n%s\n", vm.String())
+
+	// If project.yml specified any specific git commits, ensure we get them.
+	for name, ver := range vm {
+		reqs := inst.reqs[name]
+		if len(reqs) > 0 {
+			keep, err := inst.shouldKeepCommit(name, reqs[0].Ver.Commit)
+			if err != nil {
+				return nil, err
+			}
+			if keep {
+				ver.Commit = reqs[0].Ver.Commit
+			}
+			vm[name] = ver
+		}
+	}
+
+	// Now that we know which repo versions we want, we can eliminate some
+	// false-positives from the repo list.
+	repoList = inst.ensureDepsInList(candidates, vm)
+	vm = filterVersionMap(vm, repoList)
+
+	return vm, nil
+}
+
+// Installs the specified set of repos.
+func (inst *Installer) Install(
+	candidates []*repo.Repo, force bool, ask bool) error {
+
+	vm, err := inst.calcVersionMap(candidates)
+	if err != nil {
+		return err
+	}
+
+	// Perform some additional filtering on the list of repos to process.
+	if !force {
+		// Don't install a repo if it is already installed (any version).  We
+		// skip this filter for forced reinstalls.
+		vm, err = inst.filterInstallList(vm)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Notify the user of what install operations are about to happen, and
+	// prompt if the `-a` (ask) option was specified.
+	proceed, err := inst.installPrompt(vm, INSTALL_OP_INSTALL, force, ask)
+	if err != nil {
+		return err
+	}
+	if !proceed {
+		return nil
+	}
+
+	repos, err := inst.versionMapRepos(vm)
+	if err != nil {
+		return err
+	}
+
+	// For a forced install, delete all existing repos.
+	if force {
+		for _, r := range repos {
+			// Don't delete the local project directory!
+			if !r.IsLocal() {
+				util.StatusMessage(util.VERBOSITY_DEFAULT,
+					"Removing old copy of \"%s\" (%s)\n", r.Name(), r.Path())
+				os.RemoveAll(r.Path())
+				delete(inst.vers, r.Name())
+			}
+		}
+	}
+
+	// Install each repo in the version map.
+	for _, r := range repos {
+		destVer := vm[r.Name()]
+		if err := r.Install(destVer); err != nil {
+			return err
+		}
+
+		util.StatusMessage(util.VERBOSITY_DEFAULT,
+			"%s successfully installed version %s\n",
+			r.Name(), destVer.String())
+	}
+
+	return nil
+}
+
+// Installs or upgrades the specified set of repos.
+func (inst *Installer) Upgrade(candidates []*repo.Repo, ask bool) error {
+	vm, err := inst.calcVersionMap(candidates)
+	if err != nil {
+		return err
+	}
+
+	// Don't upgrade a repo if we already have the desired version.
+	vm, err = inst.filterUpgradeList(vm)
+	if err != nil {
+		return err
+	}
+
+	// Notify the user of what install operations are about to happen, and
+	// prompt if the `-a` (ask) option was specified.
+	proceed, err := inst.installPrompt(vm, INSTALL_OP_UPGRADE, false, ask)
+	if err != nil {
+		return err
+	}
+	if !proceed {
+		return nil
+	}
+
+	repos, err := inst.versionMapRepos(vm)
+	if err != nil {
+		return err
+	}
+
+	// Upgrade each repo in the version map.
+	for _, r := range repos {
+		destVer := vm[r.Name()]
+		if err := r.Upgrade(destVer); err != nil {
+			return err
+		}
+		util.StatusMessage(util.VERBOSITY_DEFAULT,
+			"%s successfully upgraded to version %s\n",
+			r.Name(), destVer.String())
+	}
+
+	return nil
+}
+
+// Syncs the specified set of repos.
+func (inst *Installer) Sync(candidates []*repo.Repo, ask bool) error {
+	vm, err := inst.calcVersionMap(candidates)
+	if err != nil {
+		return err
+	}
+
+	// Notify the user of what install operations are about to happen, and
+	// prompt if the `-a` (ask) option was specified.
+	proceed, err := inst.installPrompt(vm, INSTALL_OP_SYNC, false, ask)
+	if err != nil {
+		return err
+	}
+	if !proceed {
+		return nil
+	}
+
+	repos, err := inst.versionMapRepos(vm)
+	if err != nil {
+		return err
+	}
+
+	// Sync each repo in the list.
+	var anyFails bool
+	for _, r := range repos {
+		ver := inst.installedVer(r.Name())
+		if ver == nil {
+			util.StatusMessage(util.VERBOSITY_DEFAULT,
+				"No installed version of %s found, skipping\n",
+				r.Name())
+		} else {
+			if _, err := r.Sync(*ver); err != nil {
+				util.StatusMessage(util.VERBOSITY_QUIET,
+					"Failed to sync repo \"%s\": %s\n",
+					r.Name(), err.Error())
+				anyFails = true
+			}
+		}
+	}
+
+	if anyFails {
+		return util.FmtNewtError("Failed to sync")
+	}
+
+	return nil
+}
diff --git a/newt/newtutil/newtutil.go b/newt/newtutil/newtutil.go
index d15770d9..a976b7e3 100644
--- a/newt/newtutil/newtutil.go
+++ b/newt/newtutil/newtutil.go
@@ -175,22 +175,24 @@ func MakeTempRepoDir() (string, error) {
 	return tmpdir, nil
 }
 
-// Read in the configuration file specified by name, in path
-// return a new viper config object if successful, and error if not
-func ReadConfig(path string, name string) (ycfg.YCfg, error) {
-	fullPath := path + "/" + name + ".yml"
-
-	file, err := ioutil.ReadFile(fullPath)
+func ReadConfigPath(path string) (ycfg.YCfg, error) {
+	file, err := ioutil.ReadFile(path)
 	if err != nil {
 		return nil, util.NewNewtError(fmt.Sprintf("Error reading %s: %s",
-			fullPath, err.Error()))
+			path, err.Error()))
 	}
 
 	settings := map[string]interface{}{}
 	if err := yaml.Unmarshal(file, &settings); err != nil {
 		return nil, util.FmtNewtError("Failure parsing \"%s\": %s",
-			fullPath, err.Error())
+			path, err.Error())
 	}
 
 	return ycfg.NewYCfg(settings)
 }
+
+// Read in the configuration file specified by name, in path
+// return a new viper config object if successful, and error if not
+func ReadConfig(dir string, filename string) (ycfg.YCfg, error) {
+	return ReadConfigPath(dir + "/" + filename + ".yml")
+}
diff --git a/newt/newtutil/repo_version.go b/newt/newtutil/repo_version.go
index 3271c405..ea6bac67 100644
--- a/newt/newtutil/repo_version.go
+++ b/newt/newtutil/repo_version.go
@@ -32,11 +32,15 @@ import (
 )
 
 const (
-	VERSION_STABILITY_NONE   = "none"
+	VERSION_STABILITY_NONE   = ""
 	VERSION_STABILITY_STABLE = "stable"
 	VERSION_STABILITY_DEV    = "dev"
 	VERSION_STABILITY_LATEST = "latest"
-	VERSION_STABILITY_TAG    = "tag"
+
+	// "commit" is not actually a stability, but it takes the place of one in
+	// the repo version notation.  The "commit" string indicates a commit hash,
+	// tag, or branch, rather than a version specifier.
+	VERSION_STABILITY_COMMIT = "commit"
 )
 
 // Represents an unspecified part in a version.  For example, in "1-latest",
@@ -53,7 +57,11 @@ type RepoVersion struct {
 	Minor     int64
 	Revision  int64
 	Stability string
-	Tag       string
+	Commit    string
+}
+
+func (v *RepoVersion) IsNormalized() bool {
+	return v.Stability == VERSION_STABILITY_NONE
 }
 
 func (vm *RepoVersionReq) String() string {
@@ -73,15 +81,11 @@ func CompareRepoVersions(v1 RepoVersion, v2 RepoVersion) int64 {
 		return r
 	}
 
-	if v1.Tag != v2.Tag {
-		return 1
-	}
-
 	return 0
 }
 
 func (v *RepoVersion) Satisfies(verReq RepoVersionReq) bool {
-	if verReq.Ver.Tag != "" && verReq.CompareType != "==" {
+	if verReq.Ver.Commit != "" && verReq.CompareType != "==" {
 		log.Warningf("RepoVersion comparison with a tag %s %s %s",
 			verReq.Ver, verReq.CompareType, v)
 	}
@@ -127,10 +131,6 @@ func (v *RepoVersion) SatisfiesAll(verReqs []RepoVersionReq) bool {
 }
 
 func (ver *RepoVersion) String() string {
-	if ver.Tag != "" {
-		return fmt.Sprintf("%s-tag", ver.Tag)
-	}
-
 	s := fmt.Sprintf("%d", ver.Major)
 	if ver.Minor != VERSION_FLOATING {
 		s += fmt.Sprintf(".%d", ver.Minor)
@@ -143,6 +143,10 @@ func (ver *RepoVersion) String() string {
 		s += fmt.Sprintf("-%s", ver.Stability)
 	}
 
+	if ver.Commit != "" {
+		s += fmt.Sprintf("/%s", ver.Commit)
+	}
+
 	return s
 }
 
@@ -157,34 +161,37 @@ func (ver *RepoVersion) ToNuVersion() Version {
 func ParseRepoVersion(verStr string) (RepoVersion, error) {
 	var err error
 
-	// Split to get stability level first
-	sparts := strings.Split(verStr, "-")
 	stability := VERSION_STABILITY_NONE
-	if len(sparts) > 1 {
-		stability = strings.Trim(sparts[1], " ")
+	base := verStr
+
+	dashIdx := strings.LastIndex(verStr, "-")
+	if dashIdx != -1 {
+		stability = strings.TrimSpace(verStr[dashIdx+1:])
+		base = strings.TrimSpace(verStr[:dashIdx])
+
 		switch stability {
-		case VERSION_STABILITY_TAG:
-			return NewTag(strings.Trim(sparts[0], " ")), nil
+		case VERSION_STABILITY_COMMIT:
+			return RepoVersion{Commit: strings.TrimSpace(base)}, nil
+
 		case VERSION_STABILITY_STABLE:
-			fallthrough
 		case VERSION_STABILITY_DEV:
-			fallthrough
 		case VERSION_STABILITY_LATEST:
+
 		default:
 			return RepoVersion{}, util.FmtNewtError(
 				"Unknown stability (%s) in version %s", stability, verStr)
 		}
 	}
-	parts := strings.Split(sparts[0], ".")
+
+	parts := strings.Split(base, ".")
 	if len(parts) > 3 {
 		return RepoVersion{},
 			util.FmtNewtError("Invalid version string: %s", verStr)
 	}
 
-	if strings.Trim(parts[0], " ") == "" ||
-		strings.Trim(parts[0], " ") == "none" {
-
-		return RepoVersion{}, nil
+	if len(parts) != 3 && stability == VERSION_STABILITY_NONE {
+		return RepoVersion{},
+			util.FmtNewtError("Invalid version string: %s", verStr)
 	}
 
 	// Assume no parts of the version are specified.
@@ -195,7 +202,7 @@ func ParseRepoVersion(verStr string) (RepoVersion, error) {
 		Stability: stability,
 	}
 
-	// convert first string to an int
+	// Convert each dot-delimited part to an integer.
 	if ver.Major, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
 		return RepoVersion{}, util.NewNewtError(err.Error())
 	}
@@ -213,13 +220,6 @@ func ParseRepoVersion(verStr string) (RepoVersion, error) {
 	return ver, nil
 }
 
-func NewTag(tag string) RepoVersion {
-	return RepoVersion{
-		Tag:       tag,
-		Stability: VERSION_STABILITY_NONE,
-	}
-}
-
 // Parse a set of version string constraints on a dependency.
 // This function
 // The version string contains a list of version constraints in the following format:
diff --git a/newt/project/project.go b/newt/project/project.go
index 48b4c17a..7289abfb 100644
--- a/newt/project/project.go
+++ b/newt/project/project.go
@@ -20,12 +20,10 @@
 package project
 
 import (
-	"bufio"
 	"fmt"
 	"os"
 	"path"
 	"path/filepath"
-	"sort"
 	"strings"
 
 	log "github.com/Sirupsen/logrus"
@@ -33,6 +31,7 @@ import (
 	"mynewt.apache.org/newt/newt/compat"
 	"mynewt.apache.org/newt/newt/deprepo"
 	"mynewt.apache.org/newt/newt/downloader"
+	"mynewt.apache.org/newt/newt/install"
 	"mynewt.apache.org/newt/newt/interfaces"
 	"mynewt.apache.org/newt/newt/newtutil"
 	"mynewt.apache.org/newt/newt/pkg"
@@ -59,8 +58,6 @@ type Project struct {
 
 	packages interfaces.PackageList
 
-	projState *ProjectState
-
 	// Contains all the repos that form this project.  Each repo is in one of
 	// two states:
 	//    * description: Only the repo's basic description fields have been
@@ -80,17 +77,13 @@ type Project struct {
 
 	warnings []string
 
+	// Indicates the repos whose version we couldn't detect.  Prevents
+	// duplicate warnings.
+	unknownRepoVers map[string]struct{}
+
 	yc ycfg.YCfg
 }
 
-type installOp int
-
-const (
-	INSTALL_OP_INSTALL installOp = iota
-	INSTALL_OP_UPGRADE
-	INSTALL_OP_SYNC
-)
-
 func initProject(dir string) error {
 	var err error
 
@@ -192,9 +185,49 @@ func (proj *Project) FindRepoPath(rname string) string {
 	return r.Path()
 }
 
+func (proj *Project) GetRepoVersion(
+	rname string) (*newtutil.RepoVersion, error) {
+
+	// First, try to read the repo's `version.yml` file.
+	r := proj.repos[rname]
+	if r == nil {
+		return nil, nil
+	}
+
+	ver, err := r.InstalledVersion()
+	if err != nil {
+		return nil, err
+	}
+
+	if ver == nil {
+		commit, err := r.CurrentHash()
+		if err != nil {
+			return nil, err
+		}
+		if proj.unknownRepoVers == nil {
+			proj.unknownRepoVers = map[string]struct{}{}
+		}
+
+		if _, ok := proj.unknownRepoVers[rname]; !ok {
+			proj.unknownRepoVers[rname] = struct{}{}
+
+			util.StatusMessage(util.VERBOSITY_QUIET,
+				"WARNING: Could not detect version of installed repo \"%s\"; "+
+					"assuming 0.0.0/%s\n", r.Name(), commit)
+		}
+		ver = &newtutil.RepoVersion{
+			Commit: commit,
+		}
+	}
+
+	return ver, nil
+}
+
+// XXX: Incorrect comment.
 // Indicates whether the specified repo is present in the `project.state` file.
 func (proj *Project) RepoIsInstalled(rname string) bool {
-	return proj.projState.GetInstalledVersion(rname) != nil
+	ver, err := proj.GetRepoVersion(rname)
+	return err == nil && ver != nil
 }
 
 func (proj *Project) RepoIsRoot(rname string) bool {
@@ -224,254 +257,6 @@ func (proj *Project) SelectRepos(pred func(r *repo.Repo) bool) []*repo.Repo {
 	return filtered
 }
 
-// Indicates whether a repo should be installed.  A repo should be installed if
-// it is not currently installed.
-func (proj *Project) shouldInstallRepo(repoName string) bool {
-	return proj.projState.GetInstalledVersion(repoName) == nil
-}
-
-// Indicates whether a repo should be upgraded to the specified version.  A
-// repo should be upgraded if it is not currently installed, or if a version
-// other than the desired one is installed.
-func (proj *Project) shouldUpgradeRepo(repoName string,
-	destVer newtutil.RepoVersion) bool {
-
-	r := proj.repos[repoName]
-	if r == nil {
-		return false
-	}
-
-	stateVer := proj.projState.GetInstalledVersion(repoName)
-	if stateVer == nil {
-		return true
-	}
-
-	return !r.VersionsEqual(*stateVer, destVer)
-}
-
-// Removes repos that shouldn't be installed from the specified list.  A repo
-// should not be installed if it is already installed (any version).
-//
-// @param repos                 The list of repos to filter.
-//
-// @return []*Repo              The filtered list of repos.
-func (proj *Project) filterInstallList(repos []*repo.Repo) []*repo.Repo {
-	keep := []*repo.Repo{}
-
-	for _, r := range repos {
-		curVer := proj.projState.GetInstalledVersion(r.Name())
-		if curVer == nil {
-			keep = append(keep, r)
-		} else {
-			util.StatusMessage(util.VERBOSITY_DEFAULT,
-				"Skipping \"%s\": already installed (%s)\n",
-				r.Name(), curVer.String())
-		}
-	}
-
-	return keep
-}
-
-// Removes repos that shouldn't be upgraded from the specified list.  A repo
-// should not be upgraded if the desired version is already installed.
-//
-// @param repos                 The list of repos to filter.
-// @param vm                    Specifies the desired version of each repo.
-//
-// @return []*Repo              The filtered list of repos.
-func (proj *Project) filterUpgradeList(
-	repos []*repo.Repo, vm deprepo.VersionMap) []*repo.Repo {
-
-	keep := []*repo.Repo{}
-
-	for _, r := range repos {
-		destVer := vm[r.Name()]
-		if proj.shouldUpgradeRepo(r.Name(), destVer) {
-			keep = append(keep, r)
-		} else {
-			curVer := proj.projState.GetInstalledVersion(r.Name())
-			util.StatusMessage(util.VERBOSITY_DEFAULT,
-				"Skipping \"%s\": already upgraded (%s)\n",
-				r.Name(), curVer.String())
-		}
-	}
-
-	return keep
-}
-
-// Determines if the `project.yml` file specifies a nonexistent repo version.
-// Only the repos in the specified slice are considered.
-//
-// @param repos                 The list of repos to consider during the check.
-// @param m                     A matrix containing all versions of the
-//                                  specified repos.
-//
-// @return error                Error if any repo requirement is invalid.
-func (proj *Project) detectIllegalRepoReqs(
-	repos []*repo.Repo, m deprepo.Matrix) error {
-
-	var lines []string
-	for _, r := range repos {
-		reqs, ok := proj.rootRepoReqs[r.Name()]
-		if ok {
-			row := m.FindRow(r.Name())
-			if row == nil {
-				return util.FmtNewtError(
-					"internal error; repo \"%s\" missing from matrix", r.Name())
-			}
-
-			r := proj.repos[r.Name()]
-			nreqs, err := r.NormalizeVerReqs(reqs)
-			if err != nil {
-				return err
-			}
-
-			anySatisfied := false
-			for _, ver := range row.Vers {
-				if ver.SatisfiesAll(nreqs) {
-					anySatisfied = true
-					break
-				}
-			}
-			if !anySatisfied {
-				line := fmt.Sprintf("    %s,%s", r.Name(),
-					newtutil.RepoVerReqsString(nreqs))
-				lines = append(lines, line)
-			}
-		}
-	}
-
-	if len(lines) > 0 {
-		sort.Strings(lines)
-		return util.NewNewtError(
-			"project.yml file specifies nonexistent repo versions:\n" +
-				strings.Join(lines, "\n"))
-	}
-
-	return nil
-}
-
-// Installs or upgrades a single repo to the specified version.
-func (proj *Project) installRepo(r *repo.Repo, ver newtutil.RepoVersion,
-	upgrade bool, force bool) error {
-
-	// Install the acceptable version.
-	if upgrade {
-		if err := r.Upgrade(ver, force); err != nil {
-			return err
-		}
-		util.StatusMessage(util.VERBOSITY_DEFAULT,
-			"%s successfully upgraded to version %s\n",
-			r.Name(), ver.String())
-	} else {
-		if err := r.Install(ver); err != nil {
-			return err
-		}
-
-		util.StatusMessage(util.VERBOSITY_DEFAULT,
-			"%s successfully installed version %s\n",
-			r.Name(), ver.String())
-	}
-
-	// Update the project state with the new repository version
-	// information.
-	proj.projState.Replace(r.Name(), ver)
-
-	return nil
-}
-
-func (proj *Project) installMessageOneRepo(
-	repoName string, op installOp, force bool, curVer *newtutil.RepoVersion,
-	destVer newtutil.RepoVersion) (string, error) {
-
-	// If the repo isn't installed yet, this is an install, not an upgrade.
-	if op == INSTALL_OP_UPGRADE && curVer == nil {
-		op = INSTALL_OP_INSTALL
-	}
-
-	var verb string
-	switch op {
-	case INSTALL_OP_INSTALL:
-		if !force {
-			verb = "install"
-		} else {
-			verb = "reinstall"
-		}
-
-	case INSTALL_OP_UPGRADE:
-		verb = "upgrade"
-
-	case INSTALL_OP_SYNC:
-		verb = "sync"
-
-	default:
-		return "", util.FmtNewtError(
-			"internal error: invalid install op: %v", op)
-	}
-
-	msg := fmt.Sprintf("    %s %s ", verb, repoName)
-	if curVer != nil {
-		msg += fmt.Sprintf("(%s --> %s)", curVer.String(), destVer.String())
-	} else {
-		msg += fmt.Sprintf("(%s)", destVer.String())
-	}
-
-	return msg, nil
-}
-
-// Describes an imminent repo operation to the user.  In addition, prompts the
-// user for confirmation if the `-a` (ask) option was specified.
-func (proj *Project) installPrompt(repoList []*repo.Repo,
-	vm deprepo.VersionMap, op installOp, force bool, ask bool) (bool, error) {
-
-	if len(repoList) == 0 {
-		return true, nil
-	}
-
-	util.StatusMessage(util.VERBOSITY_DEFAULT,
-		"Making the following changes to the project:\n")
-
-	for _, r := range repoList {
-		curVer := proj.projState.GetInstalledVersion(r.Name())
-		destVer := vm[r.Name()]
-
-		msg, err := proj.installMessageOneRepo(
-			r.Name(), op, force, curVer, destVer)
-		if err != nil {
-			return false, err
-		}
-
-		util.StatusMessage(util.VERBOSITY_DEFAULT, "%s\n", msg)
-	}
-	util.StatusMessage(util.VERBOSITY_DEFAULT, "\n")
-
-	if !ask {
-		return true, nil
-	}
-
-	for {
-		fmt.Printf("Proceed? [Y/n] ")
-		line, more, err := bufio.NewReader(os.Stdin).ReadLine()
-		if more || err != nil {
-			return false, util.ChildNewtError(err)
-		}
-
-		trimmed := strings.ToLower(strings.TrimSpace(string(line)))
-		if len(trimmed) == 0 || strings.HasPrefix(trimmed, "y") {
-			// User wants to proceed.
-			return true, nil
-		}
-
-		if strings.HasPrefix(trimmed, "n") {
-			// User wants to cancel.
-			return false, nil
-		}
-
-		// Invalid response.
-		fmt.Printf("Invalid response.\n")
-	}
-}
-
 // Installs or upgrades repos matching the specified predicate.
 func (proj *Project) InstallIf(
 	upgrade bool, force bool, ask bool,
@@ -488,104 +273,19 @@ func (proj *Project) InstallIf(
 	// Determine which repos the user wants to install or upgrade.
 	specifiedRepoList := proj.SelectRepos(predicate)
 
-	// Repos that depend on any specified repos must also be considered during
-	// the install / upgrade operation.
-	repoList := proj.ensureDepsInList(specifiedRepoList, nil)
-
-	// Construct a table of all published repo versions.
-	m, err := deprepo.BuildMatrix(
-		repoList, proj.projState.AllInstalledVersions())
-	if err != nil {
-		return err
-	}
-
-	// If the `project.yml` file specifies an invalid repo version, abort now.
-	if err := proj.detectIllegalRepoReqs(repoList, m); err != nil {
-		return err
-	}
-
-	// Remove blocked repo versions from the table.
-	if err := deprepo.PruneMatrix(
-		&m, proj.repos, proj.rootRepoReqs); err != nil {
-
-		return err
-	}
-
-	// Construct a repo dependency graph from the `project.yml` version
-	// requirements and from each repo's dependency list.
-	dg, err := deprepo.BuildDepGraph(proj.repos, proj.rootRepoReqs)
+	inst, err := install.NewInstaller(proj.repos, proj.rootRepoReqs)
 	if err != nil {
 		return err
 	}
 
-	// Try to find a version set that satisfies the dependency graph.  If no
-	// such set exists, report the conflicts and abort.
-	vm, conflicts := deprepo.FindAcceptableVersions(m, dg)
-	if vm == nil {
-		return deprepo.ConflictError(conflicts)
-	}
-
-	// Now that we know which repo versions we want, we can eliminate some
-	// false-positives from the repo list.
-	repoList = proj.ensureDepsInList(specifiedRepoList, vm)
-
-	// Perform some additional filtering on the list of repos to process.
 	if upgrade {
-		// Don't upgrade a repo if we already have the desired version.
-		repoList = proj.filterUpgradeList(repoList, vm)
-	} else if !force {
-		// Don't install a repo if it is already installed (any version).  We
-		// skip this filter for forced reinstalls.
-		repoList = proj.filterInstallList(repoList)
-	}
-
-	// Notify the user of what install operations are about to happen, and
-	// prompt if the `-a` (ask) option was specified.
-	var op installOp
-	if upgrade {
-		op = INSTALL_OP_UPGRADE
+		return inst.Upgrade(specifiedRepoList, ask)
 	} else {
-		op = INSTALL_OP_INSTALL
-	}
-	proceed, err := proj.installPrompt(repoList, vm, op, force, ask)
-	if err != nil {
-		return err
-	}
-	if !proceed {
-		return nil
-	}
-
-	// For a forced install, delete all existing repos.
-	if !upgrade && force {
-		for _, r := range repoList {
-			// Don't delete the local project directory!
-			if !r.IsLocal() {
-				util.StatusMessage(util.VERBOSITY_DEFAULT,
-					"Removing old copy of \"%s\" (%s)\n", r.Name(), r.Path())
-				os.RemoveAll(r.Path())
-				proj.projState.Delete(r.Name())
-			}
-		}
+		return inst.Install(specifiedRepoList, force, ask)
 	}
-
-	// Install or upgrade each repo in the list.
-	for _, r := range repoList {
-		destVer := vm[r.Name()]
-		if err := proj.installRepo(r, destVer, upgrade, force); err != nil {
-			return err
-		}
-	}
-
-	// Save the project state, including any updates or changes to the project
-	// information that either install or upgrade caused.
-	if err := proj.projState.Save(); err != nil {
-		return err
-	}
-
-	return nil
 }
 
-// Syncs (i.e., git pulls) repos matching the specified predicate.
+// Syncs (i.e., applies `git pull` to) repos matching the specified predicate.
 func (proj *Project) SyncIf(
 	force bool, ask bool, predicate func(r *repo.Repo) bool) error {
 
@@ -594,55 +294,15 @@ func (proj *Project) SyncIf(
 		return err
 	}
 
-	// A sync operation never changes repo versions in the state file, so we
-	// can proceed with the currently-installed versions.
-	vm := proj.projState.AllInstalledVersions()
-
 	// Determine which repos the user wants to sync.
 	repoList := proj.SelectRepos(predicate)
 
-	// Repos that depend on any specified repos must also be considered during
-	// the sync operation.
-	repoList = proj.ensureDepsInList(repoList, vm)
-
-	// Notify the user of what install operations are about to happen, and
-	// prompt if the `-a` (ask) option was specified.
-	proceed, err := proj.installPrompt(
-		repoList, vm, INSTALL_OP_SYNC, force, ask)
+	inst, err := install.NewInstaller(proj.repos, proj.rootRepoReqs)
 	if err != nil {
 		return err
 	}
-	if !proceed {
-		return nil
-	}
 
-	// Sync each repo in the list.
-	var anyFails bool
-	for _, r := range repoList {
-		ver, ok := vm[r.Name()]
-		if !ok {
-			util.StatusMessage(util.VERBOSITY_DEFAULT,
-				"No installed version of %s found, skipping\n",
-				r.Name())
-		} else {
-			if _, err := r.Sync(ver, force); err != nil {
-				util.StatusMessage(util.VERBOSITY_QUIET,
-					"Failed to sync repo \"%s\": %s\n",
-					r.Name(), err.Error())
-				anyFails = true
-			}
-		}
-	}
-
-	if anyFails {
-		var forceMsg string
-		if !force {
-			forceMsg = ".  To force resync, add the -f (force) option."
-		}
-		return util.FmtNewtError("Failed to sync%s", forceMsg)
-	}
-
-	return nil
+	return inst.Sync(repoList, ask)
 }
 
 // Loads a complete repo definition from the appropriate `repository.yml` file.
@@ -684,7 +344,10 @@ func (proj *Project) loadRepo(name string, fields map[string]string) (
 	}
 
 	// Warn the user about incompatibilities with this version of newt.
-	ver := proj.projState.GetInstalledVersion(name)
+	ver, err := proj.GetRepoVersion(name)
+	if err != nil {
+		return nil, err
+	}
 	if ver != nil {
 		code, msg := r.CheckNewtCompatibility(*ver, newtutil.NewtVersion)
 		switch code {
@@ -742,7 +405,7 @@ func (proj *Project) loadRepoDeps(download bool) error {
 	loadDeps := func(r *repo.Repo) ([]*repo.Repo, error) {
 		var newRepos []*repo.Repo
 
-		depMap := r.BranchDepMap()
+		depMap := r.CommitDepMap()
 		for _, depSlice := range depMap {
 			for _, dep := range depSlice {
 				if _, ok := seen[dep.Name]; !ok {
@@ -805,53 +468,6 @@ func (proj *Project) downloadRepositoryYmlFiles() error {
 	return nil
 }
 
-// Given a slice of repos, recursively appends all depended-on repos, ensuring
-// each element is unique.
-//
-// @param repos                 The list of dependent repos to process.
-// @param vm                    Indicates the version of each repo to consider.
-//                                  Pass nil to consider all versions of all
-//                                  repos.
-//
-// @return []*repo.Repo         The original list, augmented with all
-//                                  depended-on repos.
-func (proj *Project) ensureDepsInList(repos []*repo.Repo,
-	vm deprepo.VersionMap) []*repo.Repo {
-
-	seen := map[string]struct{}{}
-
-	var recurse func(r *repo.Repo) []*repo.Repo
-	recurse = func(r *repo.Repo) []*repo.Repo {
-		// Don't process this repo a second time.
-		if _, ok := seen[r.Name()]; ok {
-			return nil
-		}
-		seen[r.Name()] = struct{}{}
-
-		result := []*repo.Repo{r}
-
-		var deps []*repo.RepoDependency
-		if vm == nil {
-			deps = r.AllDeps()
-		} else {
-			deps = r.DepsForVersion(vm[r.Name()])
-		}
-		for _, d := range deps {
-			depRepo := proj.repos[d.Name]
-			result = append(result, recurse(depRepo)...)
-		}
-
-		return result
-	}
-
-	deps := []*repo.Repo{}
-	for _, r := range repos {
-		deps = append(deps, recurse(r)...)
-	}
-
-	return deps
-}
-
 func (proj *Project) loadConfig() error {
 	yc, err := newtutil.ReadConfig(proj.BasePath,
 		strings.TrimSuffix(PROJECT_FILE_NAME, ".yml"))
@@ -863,11 +479,6 @@ func (proj *Project) loadConfig() error {
 	// we need to process it later.
 	proj.yc = yc
 
-	proj.projState, err = LoadProjectState()
-	if err != nil {
-		return err
-	}
-
 	proj.name = yc.GetValString("project.name", nil)
 
 	// Local repository always included in initialization
@@ -1056,15 +667,7 @@ func (proj *Project) loadPackageList() error {
 	repos := proj.Repos()
 	for name, repo := range repos {
 		list, warnings, err := pkg.ReadLocalPackages(repo, repo.Path())
-		if err != nil {
-			/* Failed to read the repo's package list.  Report the failure as a
-			 * warning if the project state indicates that this repo should be
-			 * installed.
-			 */
-			if _, ok := proj.projState.installedRepos[name]; ok {
-				util.StatusMessage(util.VERBOSITY_QUIET, err.Error()+"\n")
-			}
-		} else {
+		if err == nil {
 			proj.packages[name] = list
 		}
 
diff --git a/newt/project/projectstate.go b/newt/project/projectstate.go
deleted file mode 100644
index 28397df0..00000000
--- a/newt/project/projectstate.go
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * 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 project
-
-import (
-	"bufio"
-	"fmt"
-	"os"
-	"strings"
-
-	"mynewt.apache.org/newt/newt/deprepo"
-	"mynewt.apache.org/newt/newt/interfaces"
-	"mynewt.apache.org/newt/newt/newtutil"
-	"mynewt.apache.org/newt/util"
-)
-
-const PROJECT_STATE_FILE = "project.state"
-
-type ProjectState struct {
-	installedRepos deprepo.VersionMap
-}
-
-func (ps *ProjectState) GetInstalledVersion(rname string) *newtutil.RepoVersion {
-	v, ok := ps.installedRepos[rname]
-	if !ok {
-		return nil
-	}
-	return &v
-}
-
-func (ps *ProjectState) AllInstalledVersions() deprepo.VersionMap {
-	return ps.installedRepos
-}
-
-func (ps *ProjectState) Replace(rname string, rvers newtutil.RepoVersion) {
-	ps.installedRepos[rname] = rvers
-}
-
-func (ps *ProjectState) Delete(rname string) {
-	delete(ps.installedRepos, rname)
-}
-
-func (ps *ProjectState) StateFile() string {
-	return interfaces.GetProject().Path() + "/" + PROJECT_STATE_FILE
-}
-
-func (ps *ProjectState) Save() error {
-	file, err := os.Create(ps.StateFile())
-	if err != nil {
-		return util.NewNewtError(err.Error())
-	}
-	defer file.Close()
-
-	for k, v := range ps.installedRepos {
-		str := ""
-		if v.Tag == "" {
-			str = fmt.Sprintf("%s,%d.%d.%d\n", k, v.Major, v.Minor, v.Revision)
-		} else {
-			str = fmt.Sprintf("%s,%s-tag\n", k, v.Tag)
-		}
-		file.WriteString(str)
-	}
-
-	return nil
-}
-
-func (ps *ProjectState) Init() error {
-	ps.installedRepos = map[string]newtutil.RepoVersion{}
-
-	path := ps.StateFile()
-
-	// Read project state file.  If doesn't exist, it will be empty until
-	// somebody installs a repo
-	if util.NodeNotExist(path) {
-		return nil
-	}
-
-	file, err := os.Open(path)
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-
-	scanner := bufio.NewScanner(file)
-	for scanner.Scan() {
-		line := strings.Split(scanner.Text(), ",")
-		if len(line) != 2 {
-			return util.NewNewtError(fmt.Sprintf(
-				"Invalid format for line in project.state file: %s\n", line))
-		}
-
-		repoName := line[0]
-		repoVers, err := newtutil.ParseRepoVersion(line[1])
-		if err != nil {
-			return err
-		}
-
-		ps.installedRepos[repoName] = repoVers
-	}
-	return nil
-}
-
-func LoadProjectState() (*ProjectState, error) {
-	ps := &ProjectState{}
-	if err := ps.Init(); err != nil {
-		return nil, err
-	}
-	return ps, nil
-}
diff --git a/newt/repo/repo.go b/newt/repo/repo.go
index ba290ae1..2f77a336 100644
--- a/newt/repo/repo.go
+++ b/newt/repo/repo.go
@@ -26,7 +26,6 @@ import (
 	"path/filepath"
 	"sort"
 	"strings"
-	"time"
 
 	log "github.com/Sirupsen/logrus"
 	"github.com/spf13/cast"
@@ -43,6 +42,7 @@ const REPO_NAME_LOCAL = "local"
 const REPO_DEFAULT_PERMS = 0755
 
 const REPO_FILE_NAME = "repository.yml"
+const REPO_VER_FILE_NAME = "version.yml"
 const REPOS_DIR = "repos"
 
 type Repo struct {
@@ -54,10 +54,13 @@ type Repo struct {
 	local      bool
 	ncMap      compat.NewtCompatMap
 
-	// [branch] =>
+	// True if this repo was cloned during this invocation of newt.
+	newlyCloned bool
+
+	// commit => [dependencies]
 	deps map[string][]*RepoDependency
 
-	// version => branch
+	// version => commit
 	vers map[newtutil.RepoVersion]string
 }
 
@@ -67,34 +70,25 @@ type RepoDependency struct {
 	Fields  map[string]string
 }
 
-func (r *Repo) BranchDepMap() map[string][]*RepoDependency {
+func (r *Repo) CommitDepMap() map[string][]*RepoDependency {
 	return r.deps
 }
 
 func (r *Repo) AllDeps() []*RepoDependency {
-	branches := make([]string, 0, len(r.deps))
-	for branch, _ := range r.deps {
-		branches = append(branches, branch)
+	commits := make([]string, 0, len(r.deps))
+	for commit, _ := range r.deps {
+		commits = append(commits, commit)
 	}
-	sort.Strings(branches)
+	sort.Strings(commits)
 
 	deps := []*RepoDependency{}
-	for _, b := range branches {
+	for _, b := range commits {
 		deps = append(deps, r.deps[b]...)
 	}
 
 	return deps
 }
 
-func (r *Repo) DepsForVersion(ver newtutil.RepoVersion) []*RepoDependency {
-	branch, err := r.BranchFromVer(ver)
-	if err != nil {
-		return nil
-	}
-
-	return r.deps[branch]
-}
-
 func (r *Repo) AddIgnoreDir(ignDir string) {
 	r.ignDirs = append(r.ignDirs, ignDir)
 }
@@ -178,9 +172,17 @@ func (r *Repo) IsLocal() bool {
 	return r.local
 }
 
-func (r *Repo) repoFilePath() string {
+func (r *Repo) IsNewlyCloned() bool {
+	return r.newlyCloned
+}
+
+func RepoFilePath(repoName string) string {
 	return interfaces.GetProject().Path() + "/" + REPOS_DIR + "/" +
-		".configs/" + r.name + "/"
+		".configs/" + repoName
+}
+
+func (r *Repo) repoFilePath() string {
+	return RepoFilePath(r.name)
 }
 
 func (r *Repo) patchesFilePath() string {
@@ -188,7 +190,7 @@ func (r *Repo) patchesFilePath() string {
 		"/.patches/"
 }
 
-func (r *Repo) downloadRepo(branchName string) error {
+func (r *Repo) downloadRepo(commit string) error {
 	dl := r.downloader
 
 	tmpdir, err := newtutil.MakeTempRepoDir()
@@ -197,8 +199,8 @@ func (r *Repo) downloadRepo(branchName string) error {
 	}
 	defer os.RemoveAll(tmpdir)
 
-	// Download the git repo, returns the git repo, checked out to that branch
-	if err := dl.DownloadRepo(branchName, tmpdir); err != nil {
+	// Download the git repo, returns the git repo, checked out to that commit
+	if err := dl.DownloadRepo(commit, tmpdir); err != nil {
 		return util.FmtNewtError("Error downloading repository %s: %s",
 			r.Name(), err.Error())
 	}
@@ -211,6 +213,7 @@ func (r *Repo) downloadRepo(branchName string) error {
 		return err
 	}
 
+	r.newlyCloned = true
 	return nil
 }
 
@@ -218,13 +221,13 @@ func (r *Repo) checkExists() bool {
 	return util.NodeExist(r.Path())
 }
 
-func (r *Repo) updateRepo(branchName string) error {
-	err := r.downloader.UpdateRepo(r.Path(), branchName)
+func (r *Repo) updateRepo(commit string) error {
+	err := r.downloader.UpdateRepo(r.Path(), commit)
 	if err != nil {
 		// If the update failed because the repo directory has been deleted,
 		// clone the repo again.
 		if util.IsNotExist(err) {
-			err = r.downloadRepo(branchName)
+			err = r.downloadRepo(commit)
 		}
 		if err != nil {
 			return util.FmtNewtError(
@@ -235,225 +238,21 @@ func (r *Repo) updateRepo(branchName string) error {
 	return nil
 }
 
-func (r *Repo) cleanupRepo(branchName string) error {
-	dl := r.downloader
-	err := dl.CleanupRepo(r.Path(), branchName)
-	if err != nil {
-		return util.FmtNewtError("Error cleaning and updating: %s", err.Error())
-	}
-	return nil
-}
-
-func (r *Repo) saveLocalDiff() (string, error) {
-	dl := r.downloader
-	diff, err := dl.LocalDiff(r.Path())
-	if err != nil {
-		return "", util.FmtNewtError("Error creating diff for \"%s\" : %s",
-			r.Name(), err.Error())
-	}
-
-	// NOTE: date was not a typo: https://golang.org/pkg/time/#Time.Format
-	timenow := time.Now().Format("20060102_150405")
-	filename := r.patchesFilePath() + r.Name() + "_" + timenow + ".diff"
-
-	f, err := os.Create(filename)
-	if err != nil {
-		return "",
-			util.FmtNewtError("Error creating repo diff file \"%s\": %s", filename, err.Error())
-	}
-	defer f.Close()
-
-	_, err = f.Write(diff)
-	if err != nil {
-		return "",
-			util.FmtNewtError("Error writing repo diff file \"%s\": %s", filename, err.Error())
-	}
-
-	return filename, nil
-}
-
-func (r *Repo) currentBranch() (string, error) {
-	dl := r.downloader
-	branch, err := dl.CurrentBranch(r.Path())
-	if err != nil {
-		return "",
-			util.FmtNewtError("Error finding current branch for \"%s\" : %s",
-				r.Name(), err.Error())
-	}
-	return filepath.Base(branch), nil
-}
-
-func (r *Repo) BranchFromVer(ver newtutil.RepoVersion) (string, error) {
-	nver, err := r.NormalizeVersion(ver)
-	if err != nil {
-		return "", err
-	}
-
-	branch := r.vers[nver]
-	if branch == "" {
-		return "",
-			util.FmtNewtError("repo \"%s\" version %s does not map to a branch",
-				r.Name(), nver.String())
-	}
-
-	return branch, nil
-}
-
-func (r *Repo) CurrentVersion() (*newtutil.RepoVersion, error) {
-	branch, err := r.currentBranch()
-	if err != nil {
-		return nil, err
-	}
-
-	for _, v := range r.AllVersions() {
-		if r.vers[v] == branch {
-			return &v, nil
-		}
-	}
-
-	// No matching version.
-	return nil, nil
-}
-
-func (r *Repo) CurrentNormalizedVersion() (*newtutil.RepoVersion, error) {
-	ver, err := r.CurrentVersion()
-	if err != nil {
-		return nil, err
-	}
-	if ver == nil {
-		return nil, nil
-	}
-
-	*ver, err = r.NormalizeVersion(*ver)
-	if err != nil {
-		return nil, err
-	}
-
-	return ver, nil
-}
-
-func (r *Repo) AllVersions() []newtutil.RepoVersion {
-	var vers []newtutil.RepoVersion
-	for ver, _ := range r.vers {
-		vers = append(vers, ver)
-	}
-
-	return newtutil.SortedVersions(vers)
-}
-
-func (r *Repo) NormalizedVersions() ([]newtutil.RepoVersion, error) {
-	verMap := map[newtutil.RepoVersion]struct{}{}
-
-	for ver, _ := range r.vers {
-		nver, err := r.NormalizeVersion(ver)
-		if err != nil {
-			return nil, err
-		}
-		verMap[nver] = struct{}{}
-	}
-
-	vers := make([]newtutil.RepoVersion, 0, len(verMap))
-	for ver, _ := range verMap {
-		vers = append(vers, ver)
-	}
-
-	return vers, nil
-}
-
-// Converts the specified version to its equivalent x.x.x form for this repo.
-// For example, this might convert "0-dev" to "0.0.0" (depending on the
-// `repository.yml` file contents).
-func (r *Repo) NormalizeVersion(
-	ver newtutil.RepoVersion) (newtutil.RepoVersion, error) {
-
-	origVer := ver
-	for {
-		if ver.Stability == "" ||
-			ver.Stability == newtutil.VERSION_STABILITY_NONE {
-			return ver, nil
-		}
-		verStr := r.vers[ver]
-		if verStr == "" {
-			return ver, util.FmtNewtError(
-				"cannot normalize version \"%s\" for repo \"%s\"; "+
-					"no mapping to numeric version",
-				origVer.String(), r.Name())
-		}
-
-		nextVer, err := newtutil.ParseRepoVersion(verStr)
-		if err != nil {
-			return ver, err
-		}
-		ver = nextVer
-	}
-}
-
-// Normalizes the version component of a version requirement.
-func (r *Repo) NormalizeVerReq(verReq newtutil.RepoVersionReq) (
-	newtutil.RepoVersionReq, error) {
-
-	ver, err := r.NormalizeVersion(verReq.Ver)
-	if err != nil {
-		return verReq, err
-	}
-
-	verReq.Ver = ver
-	return verReq, nil
-}
-
-// Normalizes the version component of each specified version requirement.
-func (r *Repo) NormalizeVerReqs(verReqs []newtutil.RepoVersionReq) (
-	[]newtutil.RepoVersionReq, error) {
-
-	result := make([]newtutil.RepoVersionReq, len(verReqs))
-	for i, verReq := range verReqs {
-		n, err := r.NormalizeVerReq(verReq)
-		if err != nil {
-			return nil, err
-		}
-		result[i] = n
-	}
-
-	return result, nil
-}
-
-// Compares the two specified versions for equality.  Two versions are equal if
-// they ultimately map to the same branch.
-func (r *Repo) VersionsEqual(v1 newtutil.RepoVersion,
-	v2 newtutil.RepoVersion) bool {
-
-	if newtutil.CompareRepoVersions(v1, v2) == 0 {
-		return true
-	}
-
-	b1, err := r.BranchFromVer(v1)
-	if err != nil {
-		return false
-	}
-
-	b2, err := r.BranchFromVer(v2)
-	if err != nil {
-		return false
-	}
-
-	return b1 == b2
-}
-
 func (r *Repo) Install(ver newtutil.RepoVersion) error {
-	branch, err := r.BranchFromVer(ver)
+	commit, err := r.CommitFromVer(ver)
 	if err != nil {
 		return err
 	}
 
-	if err := r.updateRepo(branch); err != nil {
+	if err := r.updateRepo(commit); err != nil {
 		return err
 	}
 
 	return nil
 }
 
-func (r *Repo) Upgrade(ver newtutil.RepoVersion, force bool) error {
-	branch, err := r.BranchFromVer(ver)
+func (r *Repo) Upgrade(ver newtutil.RepoVersion) error {
+	commit, err := r.CommitFromVer(ver)
 	if err != nil {
 		return err
 	}
@@ -463,89 +262,45 @@ func (r *Repo) Upgrade(ver newtutil.RepoVersion, force bool) error {
 		return err
 	}
 
-	if changes && !force {
+	if changes {
 		return util.FmtNewtError(
 			"Repository \"%s\" contains local changes.  Provide the "+
 				"-f option to attempt a merge.", r.Name())
 	}
 
-	if err := r.updateRepo(branch); err != nil {
+	if err := r.updateRepo(commit); err != nil {
 		return err
 	}
 
 	return nil
 }
 
-func (r *Repo) Sync(ver newtutil.RepoVersion, force bool) (bool, error) {
-	var currBranch string
-
+func (r *Repo) Sync(ver newtutil.RepoVersion) (bool, error) {
 	// Update the repo description
 	if _, err := r.UpdateDesc(); err != nil {
 		return false, util.NewNewtError("Cannot update repository description.")
 	}
 
-	branchName, err := r.BranchFromVer(ver)
+	commit, err := r.CommitFromVer(ver)
 	if err != nil {
 		return false, err
 	}
-	if branchName == "" {
+	if commit == "" {
 		return false, util.FmtNewtError(
-			"No branch mapping for %s,%s", r.Name(), ver.String())
+			"No commit mapping for %s,%s", r.Name(), ver.String())
 	}
 
-	// Here assuming that if the branch was changed by the user,
-	// the user must know what he's doing...
-	// but, if -f is passed let's just save the work and re-clone
-	currBranch, err = r.currentBranch()
-
-	// currBranch == HEAD means we're dettached from HEAD, so
-	// ignore and move to "new" tag
-	if err != nil {
-		return false, err
-	} else if currBranch != "HEAD" && currBranch != branchName {
-		msg := "Unexpected local branch for %s: \"%s\" != \"%s\""
-		if force {
-			util.StatusMessage(util.VERBOSITY_DEFAULT,
-				msg+"\n", r.Name(), currBranch, branchName)
-		} else {
-			return false, util.FmtNewtError(
-				msg, r.Name(), currBranch, branchName)
-		}
-	}
-
-	// Don't try updating if on an invalid branch...
-	if currBranch == "HEAD" || currBranch == branchName {
-		util.StatusMessage(util.VERBOSITY_DEFAULT,
-			"Syncing repository \"%s\"... ", r.Name())
-		err = r.updateRepo(branchName)
-		if err == nil {
-			util.StatusMessage(util.VERBOSITY_DEFAULT, "success\n")
-			return true, err
-		} else {
-			util.StatusMessage(util.VERBOSITY_QUIET, "failed: %s\n",
-				err.Error())
-			if !force {
-				return false, err
-			}
-		}
-	}
-
-	filename, err := r.saveLocalDiff()
-	if err != nil {
-		return false, err
-	}
-	wd, _ := os.Getwd()
-	filename, _ = filepath.Rel(wd, filename)
-
-	util.StatusMessage(util.VERBOSITY_DEFAULT, "Saved local diff: "+
-		"\"%s\"\n", filename)
-
-	err = r.cleanupRepo(branchName)
-	if err != nil {
+	util.StatusMessage(util.VERBOSITY_DEFAULT,
+		"Syncing repository \"%s\"... ", r.Name())
+	err = r.updateRepo(commit)
+	if err == nil {
+		util.StatusMessage(util.VERBOSITY_DEFAULT, "success\n")
+		return true, err
+	} else {
+		util.StatusMessage(util.VERBOSITY_QUIET, "failed: %s\n",
+			err.Error())
 		return false, err
 	}
-
-	return true, nil
 }
 
 func (r *Repo) UpdateDesc() (bool, error) {
@@ -570,10 +325,7 @@ func (r *Repo) UpdateDesc() (bool, error) {
 	return true, nil
 }
 
-func (r *Repo) downloadRepositoryYml() error {
-	dl := r.downloader
-	dl.SetBranch("master")
-
+func (r *Repo) ensureExists() error {
 	// Clone the repo if it doesn't exist.
 	if util.NodeNotExist(r.localPath) {
 		if err := r.downloadRepo("master"); err != nil {
@@ -581,22 +333,44 @@ func (r *Repo) downloadRepositoryYml() error {
 		}
 	}
 
+	return nil
+}
+
+func (r *Repo) downloadFile(commit string, srcPath string) (string, error) {
+	dl := r.downloader
+	origCommit := dl.GetCommit()
+
+	dl.SetCommit(commit)
+	defer dl.SetCommit(origCommit)
+
+	// Clone the repo if it doesn't exist.
+	if err := r.ensureExists(); err != nil {
+		return "", err
+	}
+
 	cpath := r.repoFilePath()
-	if err := dl.FetchFile(r.localPath, REPO_FILE_NAME, cpath); err != nil {
-		util.StatusMessage(util.VERBOSITY_VERBOSE, "Download failed\n")
+	if err := os.MkdirAll(cpath, REPO_DEFAULT_PERMS); err != nil {
+		return "", util.ChildNewtError(err)
+	}
 
-		return err
+	if err := dl.FetchFile(r.localPath, srcPath, cpath); err != nil {
+		return "", util.FmtNewtError(
+			"Download of \"%s\" from repo:%s commit:%s failed: %s",
+			srcPath, r.Name(), commit, err.Error())
 	}
 
-	// also create a directory to save diffs for sync
-	cpath = r.repoFilePath()
-	if util.NodeNotExist(cpath) {
-		if err := os.MkdirAll(cpath, REPO_DEFAULT_PERMS); err != nil {
-			return util.NewNewtError(err.Error())
-		}
+	util.StatusMessage(util.VERBOSITY_VERBOSE,
+		"Download of \"%s\" from repo:%s commit:%s successful\n",
+		srcPath, r.Name(), commit)
+
+	return cpath + "/" + srcPath, nil
+}
+
+func (r *Repo) downloadRepositoryYml() error {
+	if _, err := r.downloadFile("master", REPO_FILE_NAME); err != nil {
+		return err
 	}
 
-	util.StatusMessage(util.VERBOSITY_VERBOSE, "Download successful!\n")
 	return nil
 }
 
@@ -656,14 +430,14 @@ func parseRepoDepMap(depName string,
 		}
 	}
 
-	for branch, verReqsStr := range versMap {
+	for commit, verReqsStr := range versMap {
 		verReqs, err := newtutil.ParseRepoVersionReqs(verReqsStr)
 		if err != nil {
 			return nil, util.FmtNewtError("invalid version string: %s",
 				verReqsStr)
 		}
 
-		result[branch] = &RepoDependency{
+		result[commit] = &RepoDependency{
 			Name:    depName,
 			VerReqs: verReqs,
 			Fields:  fields,
@@ -683,8 +457,8 @@ func (r *Repo) readDepRepos(yc ycfg.YCfg) error {
 					"dependency \"%s\": %s", r.Name(), depName, err.Error())
 		}
 
-		for branch, dep := range rdm {
-			r.deps[branch] = append(r.deps[branch], dep)
+		for commit, dep := range rdm {
+			r.deps[commit] = append(r.deps[commit], dep)
 		}
 	}
 
@@ -703,15 +477,15 @@ func (r *Repo) Read() error {
 	}
 
 	versMap := yc.GetValStringMapString("repo.versions", nil)
-	for versStr, branch := range versMap {
+	for versStr, commit := range versMap {
 		log.Debugf("Printing version %s for remote repo %s", versStr, r.name)
 		vers, err := newtutil.ParseRepoVersion(versStr)
 		if err != nil {
 			return err
 		}
 
-		// store branch->version mapping
-		r.vers[vers] = branch
+		// store commit->version mapping
+		r.vers[vers] = commit
 	}
 
 	if err := r.readDepRepos(yc); err != nil {
diff --git a/newt/repo/version.go b/newt/repo/version.go
new file mode 100644
index 00000000..d4ead9aa
--- /dev/null
+++ b/newt/repo/version.go
@@ -0,0 +1,472 @@
+/**
+ * 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.
+ */
+
+// Contains all repo version detection functionality.
+
+package repo
+
+import (
+	"strings"
+
+	log "github.com/Sirupsen/logrus"
+
+	"mynewt.apache.org/newt/newt/newtutil"
+	"mynewt.apache.org/newt/util"
+)
+
+var versionYmlMissing = util.FmtNewtError("version.yml missing")
+var versionYmlBad = util.FmtNewtError("version.yml bad")
+
+func versString(vers []newtutil.RepoVersion) string {
+	s := "["
+
+	for _, v := range vers {
+		if len(s) > 1 {
+			s += ","
+		}
+		s += v.String()
+	}
+
+	s += "]"
+	return s
+}
+
+func (r *Repo) DepsForVersion(ver newtutil.RepoVersion) []*RepoDependency {
+	// If the project uses a specific commit of this repo rather than a version
+	// specifier, ignore the commit string when calculating dependencies.
+	// Repos specify dependencies for tags and branches corresponding to
+	// version numbers rather than this particular commit.
+	ver.Commit = ""
+
+	commit, err := r.CommitFromVer(ver)
+	if err != nil {
+		return nil
+	}
+
+	return r.deps[commit]
+}
+
+// Removes extra information from a git commit string.  This throws away
+// information and causes some ambiguity, but it allows git commits to be
+// specified in a user-friendly manner (e.g., "mynewt_1_3_0_tag" rather than
+// "tags/mynewt_1_3_0_tag").
+func normalizeCommit(commit string) string {
+	commit = strings.TrimPrefix(commit, "tags/")
+	commit = strings.TrimPrefix(commit, "origin/")
+	commit = strings.TrimPrefix(commit, "heads/")
+	return commit
+}
+
+// Retrieves the repo's currently checked-out hash.
+func (r *Repo) CurrentHash() (string, error) {
+	dl := r.downloader
+	commit, err := dl.HashFor(r.Path(), "HEAD")
+	if err != nil {
+		return "",
+			util.FmtNewtError("Error finding current hash for \"%s\": %s",
+				r.Name(), err.Error())
+	}
+	return commit, nil
+}
+
+// Retrieves all commit strings corresponding to the repo's current state.
+func (r *Repo) CurrentCommits() ([]string, error) {
+	commits, err := r.downloader.CommitsFor(r.Path(), "HEAD")
+	if err != nil {
+		return nil, err
+	}
+
+	return commits, nil
+}
+
+// Retrieves the commit that the specified version maps to in `repository.yml`.
+// Note: This returns the specific commit in `repository.yml`; there may be
+// other commits that refer to the same point in the repo's history.
+func (r *Repo) CommitFromVer(ver newtutil.RepoVersion) (string, error) {
+	if ver.Commit != "" {
+		return ver.Commit, nil
+	}
+
+	nver, err := r.NormalizeVersion(ver)
+	if err != nil {
+		return "", err
+	}
+
+	commit := r.vers[nver]
+	if commit == "" {
+		return "",
+			util.FmtNewtError(
+				"repo \"%s\" version %s does not map to a commit",
+				r.Name(), nver.String())
+	}
+
+	return commit, nil
+}
+
+// Determines whether the two specified commits refer to the same point in the
+// repo's history.
+func (r *Repo) CommitsEquivalent(c1 string, c2 string) (bool, error) {
+	if c1 == "" {
+		return c2 == "", nil
+	} else if c2 == "" {
+		return false, nil
+	}
+
+	commits, err := r.downloader.CommitsFor(r.Path(), c1)
+	if err != nil {
+		return false, err
+	}
+
+	for _, c := range commits {
+		if c == c2 {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
+// Retrieves the unique commit hash corresponding to the specified repo
+// version.
+func (r *Repo) HashFromVer(ver newtutil.RepoVersion) (string, error) {
+	commit, err := r.CommitFromVer(ver)
+	if err != nil {
+		return "", err
+	}
+
+	hash, err := r.downloader.HashFor(r.Path(), commit)
+	if err != nil {
+		return "", err
+	}
+
+	return hash, nil
+}
+
+// Retrieves all versions that map to the specified commit string.
+// Note: This function only considers the specified commit.  If any equivalent
+// commits exist, they are not considered here.
+func (r *Repo) VersFromCommit(commit string) []newtutil.RepoVersion {
+	var vers []newtutil.RepoVersion
+
+	for v, c := range r.vers {
+		if c == commit {
+			vers = append(vers, v)
+		}
+	}
+
+	newtutil.SortVersions(vers)
+	return vers
+}
+
+// Retrieves all versions that map to any of the specified commit strings.
+// Note: This function only considers the specified commits.  If any equivalent
+// commits exist, they are not considered here.
+func (r *Repo) VersFromCommits(commits []string) []newtutil.RepoVersion {
+	var vers []newtutil.RepoVersion
+	for _, c := range commits {
+		vers = append(vers, r.VersFromCommit(normalizeCommit(c))...)
+	}
+
+	newtutil.SortVersions(vers)
+	return vers
+}
+
+// Retrieves all versions that map to the specified commit.  Versions that map
+// to equivalent commits are also included.
+func (r *Repo) VersFromEquivCommit(
+	commit string) ([]newtutil.RepoVersion, error) {
+
+	commits, err := r.downloader.CommitsFor(r.Path(), commit)
+	if err != nil {
+		return nil, err
+	}
+
+	return r.VersFromCommits(commits), nil
+}
+
+// Converts the specified versions to their equivalent x.x.x forms for this
+// repo.  For example, this might convert "0-dev" to "0.0.0" (depending on the
+// `repository.yml` file contents).
+func (r *Repo) NormalizedVersions() ([]newtutil.RepoVersion, error) {
+	verMap := map[newtutil.RepoVersion]struct{}{}
+
+	for ver, _ := range r.vers {
+		nver, err := r.NormalizeVersion(ver)
+		if err != nil {
+			return nil, err
+		}
+		verMap[nver] = struct{}{}
+	}
+
+	vers := make([]newtutil.RepoVersion, 0, len(verMap))
+	for ver, _ := range verMap {
+		vers = append(vers, ver)
+	}
+
+	return vers, nil
+}
+
+// Converts the specified version to its equivalent x.x.x form for this repo.
+// For example, this might convert "0-dev" to "0.0.0" (depending on the
+// `repository.yml` file contents).
+func (r *Repo) NormalizeVersion(
+	ver newtutil.RepoVersion) (newtutil.RepoVersion, error) {
+
+	origVer := ver
+	for {
+		if ver.Stability == "" ||
+			ver.Stability == newtutil.VERSION_STABILITY_NONE {
+			return ver, nil
+		}
+
+		verStr := r.vers[ver]
+		if verStr == "" {
+			return ver, util.FmtNewtError(
+				"cannot normalize version \"%s\" for repo \"%s\"; "+
+					"no mapping to numeric version",
+				origVer.String(), r.Name())
+		}
+
+		nextVer, err := newtutil.ParseRepoVersion(verStr)
+		if err != nil {
+			return ver, err
+		}
+		ver = nextVer
+	}
+}
+
+// Normalizes the version component of a version requirement.
+func (r *Repo) NormalizeVerReq(verReq newtutil.RepoVersionReq) (
+	newtutil.RepoVersionReq, error) {
+
+	ver, err := r.NormalizeVersion(verReq.Ver)
+	if err != nil {
+		return verReq, err
+	}
+
+	verReq.Ver = ver
+	return verReq, nil
+}
+
+// Normalizes the version component of each specified version requirement.
+func (r *Repo) NormalizeVerReqs(verReqs []newtutil.RepoVersionReq) (
+	[]newtutil.RepoVersionReq, error) {
+
+	result := make([]newtutil.RepoVersionReq, len(verReqs))
+	for i, verReq := range verReqs {
+		n, err := r.NormalizeVerReq(verReq)
+		if err != nil {
+			return nil, err
+		}
+		result[i] = n
+	}
+
+	return result, nil
+}
+
+// Compares the two specified versions for equality.  Two versions are equal if
+// they ultimately map to the same commit object.
+func (r *Repo) VersionsEqual(v1 newtutil.RepoVersion,
+	v2 newtutil.RepoVersion) bool {
+
+	if newtutil.CompareRepoVersions(v1, v2) == 0 {
+		return true
+	}
+
+	h1, err := r.HashFromVer(v1)
+	if err != nil {
+		return false
+	}
+
+	h2, err := r.HashFromVer(v2)
+	if err != nil {
+		return false
+	}
+
+	return h1 == h2
+}
+
+// Parses the `version.yml` file at the specified path.  On success, the parsed
+// version is returned.
+func parseVersionYml(path string) (newtutil.RepoVersion, error) {
+	yc, err := newtutil.ReadConfigPath(path)
+	if err != nil {
+		if util.IsNotExist(err) {
+			return newtutil.RepoVersion{}, versionYmlMissing
+		} else {
+			return newtutil.RepoVersion{}, versionYmlBad
+		}
+	}
+
+	verString := yc.GetValString("repo.version", nil)
+	if verString == "" {
+		return newtutil.RepoVersion{}, versionYmlBad
+	}
+
+	ver, err := newtutil.ParseRepoVersion(verString)
+	if err != nil || !ver.IsNormalized() {
+		return newtutil.RepoVersion{}, versionYmlBad
+	}
+
+	return ver, nil
+}
+
+// Reads and parses the `version.yml` file belonging to an installed repo.
+func (r *Repo) installedVersionYml() (*newtutil.RepoVersion, error) {
+	ver, err := parseVersionYml(r.Path() + "/" + REPO_VER_FILE_NAME)
+	if err != nil {
+		return nil, err
+	}
+
+	return &ver, nil
+}
+
+// Downloads and parses the `version.yml` file from the repo at the specified
+// commit.
+func (r *Repo) nonInstalledVersionYml(
+	commit string) (*newtutil.RepoVersion, error) {
+
+	filePath, err := r.downloadFile(commit, REPO_VER_FILE_NAME)
+	if err != nil {
+		// The download failed.  Determine if the commit string is bad or if
+		// the file just doesn't exist in that commit.
+		if _, e2 := r.downloader.CommitType(r.localPath, commit); e2 != nil {
+			// Bad commit string.
+			return nil, err
+		}
+
+		// The commit exists, but it doesn't contain a `version.yml` file.
+		// Assume the commit corresponds to version 0.0.0.
+		return nil, versionYmlMissing
+	}
+
+	ver, err := parseVersionYml(filePath)
+	if err != nil {
+		return nil, err
+	}
+
+	return &ver, nil
+}
+
+// Tries to determine which repo version meets the specified criteria:
+//  * Maps to the specified commit string (or an equivalent commit).
+//  * Is equal to the specified version read from `version.yml` (if not-null).
+func (r *Repo) inferVersion(commit string, vyVer *newtutil.RepoVersion) (
+	*newtutil.RepoVersion, error) {
+
+	// Search `repository.yml` for versions that the specified commit maps to.
+	ryVers, err := r.VersFromEquivCommit(commit)
+	if err != nil {
+		return nil, err
+	}
+
+	// If valid versions were derived from both `version.yml` and the specified
+	// commit+`repository.yml`, look for a common version.
+	if vyVer != nil {
+		if len(ryVers) > 0 {
+			for _, cv := range ryVers {
+				if newtutil.CompareRepoVersions(*vyVer, cv) == 0 {
+					return vyVer, nil
+				}
+			}
+
+			util.StatusMessage(util.VERBOSITY_QUIET,
+				"WARNING: Version mismatch in %s:%s; "+
+					"repository.yml:%s, version.yml:%s\n",
+				r.Name(), commit, versString(ryVers), vyVer.String())
+		} else {
+			// If the set of commits don't match a version from
+			// `repository.yml`, record the commit hash in the version
+			// specifier.  This will distinguish the returned version from its
+			// corresponding official release.
+			hash, err := r.downloader.HashFor(r.Path(), commit)
+			if err != nil {
+				return nil, err
+			}
+			vyVer.Commit = hash
+		}
+
+		// Always prefer the version in `version.yml`.
+		log.Debugf("Inferred version %s from %s:%s from version.yml",
+			vyVer.String(), r.Name(), commit)
+		return vyVer, nil
+	}
+
+	if len(ryVers) > 0 {
+		log.Debugf("Inferred version %s for %s:%s from repository.yml",
+			ryVers[0].String(), r.Name(), commit)
+		return &ryVers[0], nil
+	}
+
+	return nil, nil
+}
+
+// Retrieves the installed version of the repo.  Returns nil if the version
+// cannot be detected.
+func (r *Repo) InstalledVersion() (*newtutil.RepoVersion, error) {
+	vyVer, err := r.installedVersionYml()
+	if err != nil && err != versionYmlMissing && err != versionYmlBad {
+		return nil, err
+	}
+
+	hash, err := r.CurrentHash()
+	if err != nil {
+		return nil, err
+	}
+
+	ver, err := r.inferVersion(hash, vyVer)
+	if err != nil {
+		return nil, err
+	}
+
+	return ver, nil
+}
+
+// Retrieves the repo version corresponding to the specified commit.  Returns
+// nil if the version cannot be detected.
+func (r *Repo) NonInstalledVersion(
+	commit string) (*newtutil.RepoVersion, error) {
+
+	ver, versionYmlErr := r.nonInstalledVersionYml(commit)
+	if versionYmlErr != nil &&
+		versionYmlErr != versionYmlMissing &&
+		versionYmlErr != versionYmlBad {
+
+		return nil, versionYmlErr
+	}
+
+	ver, err := r.inferVersion(commit, ver)
+	if err != nil {
+		return nil, err
+	}
+
+	if ver == nil {
+		if versionYmlErr == versionYmlMissing {
+			util.StatusMessage(util.VERBOSITY_QUIET,
+				"WARNING: %s:%s does not contain a `version.yml` file.\n",
+				r.Name(), commit)
+		} else if versionYmlErr == versionYmlBad {
+			util.StatusMessage(util.VERBOSITY_QUIET,
+				"WARNING: %s:%s contains a malformed `version.yml` file.\n",
+				r.Name(), commit)
+		}
+	}
+
+	return ver, nil
+}


 

----------------------------------------------------------------
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