| /** |
| * 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 downloader |
| |
| import ( |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "sort" |
| "strconv" |
| "strings" |
| |
| log "github.com/sirupsen/logrus" |
| |
| "mynewt.apache.org/newt/newt/settings" |
| "mynewt.apache.org/newt/util" |
| ) |
| |
| type DownloaderCommitType int |
| |
| const ( |
| COMMIT_TYPE_BRANCH DownloaderCommitType = iota |
| COMMIT_TYPE_TAG |
| COMMIT_TYPE_HASH |
| ) |
| |
| type Downloader interface { |
| // Fetches all remotes and downloads the specified file. |
| FetchFile(commit string, path string, filename string, dstDir string) error |
| |
| // Clones the repo and checks out the specified commit. |
| Clone(commit string, dstPath string) error |
| |
| // Determines the equivalent commit hash for the specified commit string. |
| HashFor(path string, commit string) (string, error) |
| |
| // Collects all commits that are equivalent to the specified commit string |
| // (i.e., 1 hash, n tags, and n branches). |
| CommitsFor(path string, commit string) ([]string, error) |
| |
| // Fetches all remotes. |
| Fetch(path string) error |
| |
| // Checks out the specified commit (hash, tag, or branch). Always puts the |
| // repo in a "detached head" state. |
| Checkout(path string, commit string) error |
| |
| // Indicates whether the repo is in a clean or dirty state. |
| DirtyState(path string) (string, error) |
| |
| // Determines the type of the specified commit. |
| CommitType(path string, commit string) (DownloaderCommitType, error) |
| |
| // Configures the `origin` remote with the correct URL, according the the |
| // user's `project.yml` file and / or the repo dependency lists. |
| FixupOrigin(path string) error |
| |
| // Retrieves the name of the currently checked out local branch, or "" if |
| // the repo is in a "detached head" state. |
| CurrentBranch(path string) (string, error) |
| |
| // LatestRc finds the commit of the latest release candidate. It looks |
| // for commits with names matching the base commit string, but with with |
| // "_rc#" inserted. This is useful when a release candidate is being |
| // tested. In this case, the "rc" tags exist, but the official release |
| // tag has not been created yet. |
| // |
| // If such a commit exists, it is returned. Otherwise, "" is returned. |
| LatestRc(path string, base string) (string, error) |
| } |
| |
| type Commit struct { |
| hash string |
| name string |
| typ DownloaderCommitType |
| } |
| |
| type GenericDownloader struct { |
| // [name-of-branch-or-tag]commit |
| commits map[string]Commit |
| |
| // Hash of checked-out commit. |
| head string |
| |
| // Whether 'origin' has been fetched during this run. |
| fetched bool |
| } |
| |
| type GithubDownloader struct { |
| GenericDownloader |
| Server string |
| User string |
| Repo string |
| |
| // Login for private repos. |
| Login string |
| |
| // Password for private repos. |
| Password string |
| |
| // Name of environment variable containing the password for private repos. |
| // Only used if the Password field is empty. |
| PasswordEnv string |
| } |
| |
| type GitDownloader struct { |
| GenericDownloader |
| Url string |
| } |
| |
| type LocalDownloader struct { |
| GenericDownloader |
| |
| // Path to parent directory of repository.yml file. |
| Path string |
| } |
| |
| func gitPath() (string, error) { |
| gitPath, err := exec.LookPath("git") |
| if err != nil { |
| return "", util.NewNewtError(fmt.Sprintf("Can't find git binary: %s\n", |
| err.Error())) |
| } |
| |
| return filepath.ToSlash(gitPath), nil |
| } |
| |
| func executeGitCommand(dir string, cmd []string, logCmd bool) ([]byte, error) { |
| wd, err := os.Getwd() |
| if err != nil { |
| return nil, util.NewNewtError(err.Error()) |
| } |
| |
| gp, err := gitPath() |
| if err != nil { |
| return nil, err |
| } |
| |
| if err := os.Chdir(dir); err != nil { |
| return nil, util.ChildNewtError(err) |
| } |
| |
| defer os.Chdir(wd) |
| |
| gitCmd := []string{gp} |
| gitCmd = append(gitCmd, cmd...) |
| output, err := util.ShellCommandLimitDbgOutput(gitCmd, nil, logCmd, -1) |
| if err != nil { |
| return nil, err |
| } |
| |
| return output, nil |
| } |
| |
| func commitExists(repoDir string, commit string) bool { |
| cmd := []string{ |
| "show-ref", |
| "--verify", |
| "--quiet", |
| "refs/heads/" + commit, |
| } |
| _, err := executeGitCommand(repoDir, cmd, true) |
| return err == nil |
| } |
| |
| func initSubmodules(path string) error { |
| cmd := []string{ |
| "submodule", |
| "init", |
| } |
| |
| _, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func updateSubmodules(path string) error { |
| cmd := []string{ |
| "submodule", |
| "update", |
| } |
| |
| _, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // fixupCommitString strips "origin/" from the front of a commit, if it is |
| // present. Newt only works with remote branches, and only with the "origin" |
| // remote. The user is not required to prefix his branch specifiers with |
| // "origin/", but is allowed to. |
| func fixupCommitString(s string) string { |
| return strings.TrimPrefix(s, "origin/") |
| } |
| |
| func mergeBase(repoDir string, commit string) (string, error) { |
| cmd := []string{ |
| "merge-base", |
| commit, |
| commit, |
| } |
| o, err := executeGitCommand(repoDir, cmd, true) |
| if err != nil { |
| return "", err |
| } |
| |
| return strings.TrimSpace(string(o)), nil |
| } |
| |
| func upstreamFor(path string, commit string) (string, error) { |
| cmd := []string{ |
| "rev-parse", |
| "--abbrev-ref", |
| "--symbolic-full-name", |
| commit + "@{u}", |
| } |
| |
| up, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| if !util.IsExit(err) { |
| return "", err |
| } else { |
| return "", nil |
| } |
| } |
| |
| return strings.TrimSpace(string(up)), nil |
| } |
| |
| func getRemoteUrl(path string, remote string) (string, error) { |
| cmd := []string{ |
| "remote", |
| "get-url", |
| remote, |
| } |
| |
| o, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| return "", err |
| } |
| |
| return strings.TrimSpace(string(o)), nil |
| } |
| |
| func setRemoteUrlCmd(remote string, url string) []string { |
| return []string{ |
| "remote", |
| "set-url", |
| remote, |
| url, |
| } |
| } |
| |
| func setRemoteUrl(path string, remote string, url string, logCmd bool) error { |
| cmd := setRemoteUrlCmd(remote, url) |
| _, err := executeGitCommand(path, cmd, logCmd) |
| return err |
| } |
| |
| func warnWrongOriginUrl(path string, curUrl string, goodUrl string) { |
| util.OneTimeWarning( |
| "Repo's \"origin\" remote points to unexpected URL: "+ |
| "%s; correcting it to %s. Repo contents may be incorrect.", |
| curUrl, goodUrl) |
| } |
| |
| // getCommits gathers all tags and remote branches. It returns a mapping of |
| // [name]commit. |
| func getCommits(path string) (map[string]Commit, error) { |
| cmd := []string{"show-ref", "--dereference"} |
| o, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Example output: |
| // b7a5474d569d5b67152d1773627ddda010c080a3 refs/remotes/origin/1_7_0_dev |
| // da13fb50c3b5824c47a44b62c3c9f693b922ce9c refs/tags/mynewt_1_7_0_tag |
| // b7a5474d569d5b67152d1773627ddda010c080a3 refs/tags/mynewt_1_7_0_tag^{} |
| |
| m := map[string]Commit{} |
| |
| lines := strings.Split(strings.TrimSpace(string(o)), "\n") |
| for _, line := range lines { |
| f := strings.Fields(line) |
| if len(f) != 2 { |
| return nil, util.FmtNewtError( |
| "git show-ref produced unexpected line: \"%s\"", line) |
| } |
| |
| hash := f[0] |
| ref := strings.TrimSuffix(f[1], "^{}") |
| |
| c := Commit{ |
| hash: hash, |
| } |
| if n := strings.TrimPrefix(ref, "refs/remotes/origin/"); n != ref { |
| c.typ = COMMIT_TYPE_BRANCH |
| c.name = n |
| } else if n := strings.TrimPrefix(ref, "refs/tags/"); n != ref { |
| c.typ = COMMIT_TYPE_TAG |
| c.name = n |
| } |
| |
| if c.name != "" { |
| m[c.name] = c |
| } |
| } |
| |
| return m, nil |
| } |
| |
| // init populates a generic downloader with branch and tag information. |
| func (gd *GenericDownloader) init(path string) error { |
| cmap, err := getCommits(path) |
| if err != nil { |
| return err |
| } |
| gd.commits = cmap |
| |
| cmd := []string{"rev-parse", "HEAD"} |
| o, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| return err |
| } |
| gd.head = strings.TrimSpace(string(o)) |
| |
| return nil |
| } |
| |
| // ensureInited calls init on the provided downloader if it has not already |
| // been initialized. |
| func (gd *GenericDownloader) ensureInited(path string) error { |
| if gd.commits != nil { |
| // Already initialized. |
| return nil |
| } |
| |
| return gd.init(path) |
| } |
| |
| // untrackedFilesFromCheckoutErr collects the list of untracked files that |
| // prevented a checkout from succeeding. It returns nil if the provided error |
| // does not indicate that untracked files are in the way. |
| func untrackedFilesFromCheckoutErr(err error) []string { |
| var files []string |
| |
| text := err.Error() |
| lines := strings.Split(text, "\n") |
| |
| collecting := false |
| for _, line := range lines { |
| if !collecting { |
| if strings.Contains(line, |
| "The following untracked working tree files would "+ |
| "be overwritten by checkout:") { |
| collecting = true |
| } |
| } else { |
| if strings.Contains(line, "Please move or remove them before") { |
| collecting = false |
| } else { |
| files = append(files, strings.TrimSpace(line)) |
| } |
| } |
| } |
| |
| return files |
| } |
| |
| // applyMcubootPreHack attempts to clean up the mcuboot repo so that the |
| // subsequent checkout operation will succeed. This hack is required because a |
| // directory in the mcuboot repo was replaced with a submodule. Git is unable |
| // to transition from a post-replace commit to a pre-replace commit because |
| // some files in the submodule used to exist in the mcuboot repo itself. If |
| // this issue is detected, this function deletes the submodule directory so |
| // that the checkout operation can be attempted again. |
| func applyMcubootPreHack(repoDir string, err error) error { |
| if !strings.HasSuffix(repoDir, "repos/mcuboot") { |
| // Not the mcuboot repo. |
| return err |
| } |
| |
| // Check for an "untracked files" error. |
| files := untrackedFilesFromCheckoutErr(err) |
| if len(files) == 0 { |
| // Not a hackable error. |
| return err |
| } |
| |
| for _, file := range files { |
| if !strings.HasPrefix(file, "ext/mbedtls") { |
| return err |
| } |
| } |
| |
| path := repoDir + "/ext/mbedtls" |
| log.Debugf("applying mcuboot hack: removing %s", path) |
| os.RemoveAll(path) |
| return nil |
| } |
| |
| // applyMcubootPostHack attempts to clean up the mcuboot repo so that the |
| // subsequent checkout operation will succeed. This hack is required because a |
| // directory in the mcuboot repo was replaced with a submodule. This hack |
| // should be applied after a successful checkout from a pre-replace commit to a |
| // post-replace commit. This function deletes an orphan directory left behind |
| // by the checkout operation. |
| func applyMcubootPostHack(repoDir string, output string) { |
| if !strings.HasSuffix(repoDir, "repos/mcuboot") { |
| // Not the mcuboot repo. |
| return |
| } |
| |
| // Check for a "unable to rmdir" warning (pre- to post- submodule move). |
| if strings.Contains(output, "unable to rmdir 'sim/mcuboot-sys/mbedtls'") { |
| path := repoDir + "/sim/mcuboot-sys/mbedtls" |
| log.Debugf("applying mcuboot hack: removing %s", path) |
| os.RemoveAll(path) |
| } |
| } |
| |
| func (gd *GenericDownloader) Checkout(repoDir string, commit string) error { |
| // Get the hash corresponding to the commit in case the caller specified a |
| // branch or tag. We always want to check out a hash and end up in a |
| // "detached head" state. |
| hash, err := gd.HashFor(repoDir, commit) |
| if err != nil { |
| return err |
| } |
| |
| util.StatusMessage(util.VERBOSITY_VERBOSE, "Will checkout %s\n", hash) |
| cmd := []string{ |
| "checkout", |
| hash, |
| } |
| |
| o, err := executeGitCommand(repoDir, cmd, true) |
| if err != nil { |
| if err := applyMcubootPreHack(repoDir, err); err != nil { |
| return err |
| } |
| |
| if _, err := executeGitCommand(repoDir, cmd, true); err != nil { |
| return err |
| } |
| } else { |
| applyMcubootPostHack(repoDir, string(o)) |
| } |
| |
| // Always initialize and update submodules on checkout. This prevents the |
| // repo from being in a modified "(new commits)" state immediately after |
| // switching commits. If the submodules have already been updated, this |
| // does not generate any network activity. |
| if err := initSubmodules(repoDir); err != nil { |
| return err |
| } |
| if err := updateSubmodules(repoDir); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (gd *GenericDownloader) showFile( |
| path string, commit string, filename string, dstDir string) error { |
| |
| if err := os.MkdirAll(dstDir, os.ModePerm); err != nil { |
| return util.ChildNewtError(err) |
| } |
| |
| hash, err := gd.HashFor(path, commit) |
| if err != nil { |
| return err |
| } |
| |
| dstPath := fmt.Sprintf("%s/%s", dstDir, filename) |
| log.Debugf("Fetching file %s to %s", filename, dstPath) |
| |
| cmd := []string{"show", fmt.Sprintf("%s:%s", hash, filename)} |
| data, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| return err |
| } |
| |
| if err := ioutil.WriteFile(dstPath, data, os.ModePerm); err != nil { |
| return util.ChildNewtError(err) |
| } |
| |
| return nil |
| } |
| |
| func (gd *GenericDownloader) findCommit(s string) *Commit { |
| c, ok := gd.commits[fixupCommitString(s)] |
| if !ok { |
| return nil |
| } else { |
| return &c |
| } |
| } |
| |
| func (gd *GenericDownloader) CommitType( |
| path string, commit string) (DownloaderCommitType, error) { |
| |
| if err := gd.ensureInited(path); err != nil { |
| return -1, err |
| } |
| |
| // HEAD is always a commit hash (detached). |
| if commit == "HEAD" { |
| return COMMIT_TYPE_HASH, nil |
| } |
| |
| // Check if user provided a branch or tag name. |
| if c := gd.findCommit(commit); c != nil { |
| return c.typ, nil |
| } |
| |
| // Check if user provided a commit hash. |
| if _, err := mergeBase(path, commit); err == nil { |
| return COMMIT_TYPE_HASH, nil |
| } |
| |
| return -1, util.FmtNewtError( |
| "cannot determine commit type of \"%s\"", commit) |
| } |
| |
| func (gd *GenericDownloader) HashFor(path string, |
| commit string) (string, error) { |
| |
| if err := gd.ensureInited(path); err != nil { |
| return "", err |
| } |
| |
| if commit == "HEAD" { |
| return gd.head, nil |
| } |
| |
| if c := gd.findCommit(commit); c != nil { |
| return c.hash, nil |
| } |
| |
| return commit, nil |
| } |
| |
| func (gd *GenericDownloader) CommitsFor( |
| path string, commit string) ([]string, error) { |
| |
| if err := gd.ensureInited(path); err != nil { |
| return nil, err |
| } |
| |
| commit = fixupCommitString(commit) |
| |
| var commits []string |
| |
| // Add all commits that are equivalent to the specified string. |
| for _, c := range gd.commits { |
| if commit == c.hash { |
| // User specified a hash; add the corresponding branch or tag name. |
| commits = append(commits, c.name) |
| } else if commit == c.name { |
| // User specified a branch or tag; add the corresponding hash. |
| commits = append(commits, c.hash) |
| } |
| } |
| |
| sort.Strings(commits) |
| return commits, nil |
| } |
| |
| func (gd *GenericDownloader) CurrentBranch(path string) (string, error) { |
| // Check if there is a git ref (branch) for the current commit. If there |
| // is none, git exits with a status of 1. We need to distinguish this case |
| // from an actual error. |
| cmd := []string{"symbolic-ref", "-q", "HEAD"} |
| o, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| ne := err.(*util.NewtError) |
| ee, ok := ne.Parent.(*exec.ExitError) |
| if ok && ee.ExitCode() == 1 { |
| // No branch. |
| return "", nil |
| } else { |
| return "", err |
| } |
| } |
| |
| s := strings.TrimSpace(string(o)) |
| branch := strings.TrimPrefix(s, "refs/heads/") |
| if branch == s { |
| return "", util.FmtNewtError( |
| "%s produced unexpected output: %s", strings.Join(cmd, " "), s) |
| } |
| |
| return branch, nil |
| } |
| |
| // Fetches the downloader's origin remote if it hasn't been fetched yet during |
| // this run. |
| func (gd *GenericDownloader) cachedFetch(fn func() error) error { |
| if gd.fetched { |
| return nil |
| } |
| |
| if err := fn(); err != nil { |
| return err |
| } |
| |
| gd.fetched = true |
| return nil |
| } |
| |
| // Indicates whether the specified git repo is in a clean or dirty state. |
| // |
| // @param path The path of the git repo to check. |
| // |
| // @return string Text describing repo's dirty state, or "" if |
| // clean. |
| // @return error Error. |
| func (gd *GenericDownloader) DirtyState(path string) (string, error) { |
| // Check for local changes. |
| cmd := []string{ |
| "diff", |
| "--name-only", |
| } |
| |
| o, err := executeGitCommand(path, cmd, true) |
| if err != nil { |
| return "", err |
| } |
| |
| if len(o) > 0 { |
| return "local changes", nil |
| } |
| |
| // Check for staged changes. |
| cmd = []string{ |
| "diff", |
| "--name-only", |
| "--staged", |
| } |
| |
| o, err = executeGitCommand(path, cmd, true) |
| if err != nil { |
| return "", err |
| } |
| |
| if len(o) > 0 { |
| return "staged changes", nil |
| } |
| |
| // If on a branch with a configured upstream, check for unpushed commits. |
| branch, err := gd.CurrentBranch(path) |
| if err != nil { |
| return "", err |
| } |
| |
| upstream, err := upstreamFor(path, "HEAD") |
| if err != nil { |
| return "", err |
| } |
| |
| if upstream != "" && branch != "" { |
| cmd = []string{ |
| "rev-list", |
| "@{u}..", |
| } |
| |
| o, err = executeGitCommand(path, cmd, true) |
| if err != nil { |
| return "", err |
| } |
| |
| if len(o) > 0 { |
| return "unpushed commits", nil |
| } |
| } |
| |
| return "", nil |
| } |
| |
| func (gd *GenericDownloader) LatestRc(path string, |
| base string) (string, error) { |
| |
| if err := gd.ensureInited(path); err != nil { |
| return "", err |
| } |
| |
| // Example: |
| // [BASE] mynewt_1_7_0_tag |
| // [RC] mynewt_1_7_0_rc1_tag |
| |
| notag := strings.TrimSuffix(base, "_tag") |
| if notag == base { |
| return "", nil |
| } |
| |
| restr := fmt.Sprintf("^%s_rc(\\d+)_tag$", regexp.QuoteMeta(notag)) |
| re, err := regexp.Compile(restr) |
| if err != nil { |
| return "", util.FmtNewtError("internal error: %s", err.Error()) |
| } |
| |
| bestNum := -1 |
| bestStr := "" |
| for commit, _ := range gd.commits { |
| match := re.FindStringSubmatch(commit) |
| if len(match) >= 2 { |
| num, _ := strconv.Atoi(match[1]) |
| if num > bestNum { |
| bestNum = num |
| bestStr = commit |
| } |
| } |
| } |
| |
| return bestStr, nil |
| } |
| |
| func (gd *GithubDownloader) Fetch(repoDir string) error { |
| return gd.cachedFetch(func() error { |
| util.StatusMessage(util.VERBOSITY_VERBOSE, "Fetching repo %s\n", |
| gd.Repo) |
| |
| cmd := []string{"fetch", "--tags"} |
| _, err := gd.authenticatedCommand(repoDir, cmd) |
| return err |
| }) |
| } |
| |
| func (gd *GithubDownloader) password() string { |
| if gd.Password != "" { |
| return gd.Password |
| } else if gd.PasswordEnv != "" { |
| return os.Getenv(gd.PasswordEnv) |
| } else { |
| return "" |
| } |
| } |
| |
| func (gd *GithubDownloader) authenticatedCommand(path string, |
| args []string) ([]byte, error) { |
| |
| if err := gd.setRemoteAuth(path); err != nil { |
| return nil, err |
| } |
| defer gd.clearRemoteAuth(path) |
| |
| return executeGitCommand(path, args, true) |
| } |
| |
| func (gd *GithubDownloader) FetchFile( |
| commit string, path string, filename string, dstDir string) error { |
| |
| if err := gd.Fetch(path); err != nil { |
| return err |
| } |
| |
| if err := gd.showFile(path, commit, filename, dstDir); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (gd *GithubDownloader) remoteUrls() (string, string) { |
| server := "github.com" |
| |
| if gd.Server != "" { |
| server = gd.Server |
| } |
| |
| var auth string |
| if gd.Login != "" { |
| pw := gd.password() |
| auth = fmt.Sprintf("%s:%s@", gd.Login, pw) |
| } |
| |
| url := fmt.Sprintf("https://%s%s/%s/%s.git", auth, server, gd.User, |
| gd.Repo) |
| publicUrl := fmt.Sprintf("https://%s/%s/%s.git", server, gd.User, gd.Repo) |
| |
| return url, publicUrl |
| } |
| |
| func (gd *GithubDownloader) setOriginUrl(path string, url string) error { |
| // Hide password in logged command. |
| safeUrl := url |
| pw := gd.password() |
| if pw != "" { |
| safeUrl = strings.Replace(safeUrl, pw, "<password-hidden>", -1) |
| } |
| util.LogShellCmd(setRemoteUrlCmd("origin", safeUrl), nil) |
| |
| return setRemoteUrl(path, "origin", url, false) |
| } |
| |
| func (gd *GithubDownloader) clearRemoteAuth(path string) error { |
| url, publicUrl := gd.remoteUrls() |
| if url == publicUrl { |
| return nil |
| } |
| |
| return gd.setOriginUrl(path, publicUrl) |
| } |
| |
| func (gd *GithubDownloader) setRemoteAuth(path string) error { |
| url, publicUrl := gd.remoteUrls() |
| if url == publicUrl { |
| return nil |
| } |
| |
| return gd.setOriginUrl(path, url) |
| } |
| |
| func (gd *GithubDownloader) Clone(commit string, dstPath string) error { |
| // Currently only the master branch is supported. |
| branch := "master" |
| |
| url, publicUrl := gd.remoteUrls() |
| |
| util.StatusMessage(util.VERBOSITY_DEFAULT, |
| "Downloading repository %s (commit: %s) from %s\n", |
| gd.Repo, commit, publicUrl) |
| |
| gp, err := gitPath() |
| if err != nil { |
| return err |
| } |
| |
| // Clone the repository. |
| cmd := []string{ |
| gp, |
| "clone", |
| "-b", |
| branch, |
| url, |
| dstPath, |
| } |
| |
| if util.Verbosity >= util.VERBOSITY_VERBOSE { |
| err = util.ShellInteractiveCommand(cmd, nil, false) |
| } else { |
| _, err = util.ShellCommand(cmd, nil) |
| } |
| if err != nil { |
| return err |
| } |
| defer gd.clearRemoteAuth(dstPath) |
| |
| if err := gd.Checkout(dstPath, commit); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (gd *GithubDownloader) FixupOrigin(path string) error { |
| curUrl, err := getRemoteUrl(path, "origin") |
| if err != nil { |
| return err |
| } |
| |
| // Use the public URL, i.e., hide the login and password. |
| _, publicUrl := gd.remoteUrls() |
| if curUrl == publicUrl { |
| return nil |
| } |
| |
| warnWrongOriginUrl(path, curUrl, publicUrl) |
| return gd.setOriginUrl(path, publicUrl) |
| } |
| |
| func NewGithubDownloader() *GithubDownloader { |
| return &GithubDownloader{} |
| } |
| |
| func (gd *GitDownloader) Fetch(repoDir string) error { |
| return gd.cachedFetch(func() error { |
| _, err := executeGitCommand(repoDir, []string{"fetch", "--tags"}, true) |
| return err |
| }) |
| } |
| |
| func (gd *GitDownloader) FetchFile( |
| commit string, path string, filename string, dstDir string) error { |
| |
| if err := gd.Fetch(path); err != nil { |
| return err |
| } |
| |
| if err := gd.showFile(path, commit, filename, dstDir); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (gd *GitDownloader) Clone(commit string, dstPath string) error { |
| // Currently only the master branch is supported. |
| branch := "master" |
| |
| util.StatusMessage(util.VERBOSITY_DEFAULT, |
| "Downloading repository %s (commit: %s)\n", gd.Url, commit) |
| |
| gp, err := gitPath() |
| if err != nil { |
| return err |
| } |
| |
| // Clone the repository. |
| cmd := []string{ |
| gp, |
| "clone", |
| "-b", |
| branch, |
| gd.Url, |
| dstPath, |
| } |
| |
| if util.Verbosity >= util.VERBOSITY_VERBOSE { |
| err = util.ShellInteractiveCommand(cmd, nil, false) |
| } else { |
| _, err = util.ShellCommand(cmd, nil) |
| } |
| if err != nil { |
| return err |
| } |
| |
| if err := gd.Checkout(dstPath, commit); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (gd *GitDownloader) FixupOrigin(path string) error { |
| curUrl, err := getRemoteUrl(path, "origin") |
| if err != nil { |
| return err |
| } |
| |
| if curUrl == gd.Url { |
| return nil |
| } |
| |
| warnWrongOriginUrl(path, curUrl, gd.Url) |
| return setRemoteUrl(path, "origin", gd.Url, true) |
| } |
| |
| func NewGitDownloader() *GitDownloader { |
| return &GitDownloader{} |
| } |
| |
| func (ld *LocalDownloader) FetchFile( |
| commit string, path string, filename string, dstDir string) error { |
| |
| srcPath := ld.Path + "/" + filename |
| dstPath := dstDir + "/" + filename |
| |
| log.Debugf("Fetching file %s to %s", srcPath, dstPath) |
| if err := util.CopyFile(srcPath, dstPath); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (ld *LocalDownloader) Fetch(path string) error { |
| os.RemoveAll(path) |
| return ld.Clone("master", path) |
| } |
| |
| func (ld *LocalDownloader) Checkout(path string, commit string) error { |
| _, err := executeGitCommand(path, []string{"checkout", commit}, true) |
| return err |
| } |
| |
| func (ld *LocalDownloader) Clone(commit string, dstPath string) error { |
| util.StatusMessage(util.VERBOSITY_DEFAULT, |
| "Downloading local repository %s\n", ld.Path) |
| |
| if err := util.CopyDir(ld.Path, dstPath); err != nil { |
| return err |
| } |
| |
| if err := ld.Checkout(dstPath, commit); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (ld *LocalDownloader) FixupOrigin(path string) error { |
| return nil |
| } |
| |
| func NewLocalDownloader() *LocalDownloader { |
| return &LocalDownloader{} |
| } |
| |
| func loadError(format string, args ...interface{}) error { |
| return util.NewNewtError( |
| "error loading project.yml: " + fmt.Sprintf(format, args...)) |
| } |
| |
| func LoadDownloader(repoName string, repoVars map[string]string) ( |
| Downloader, error) { |
| |
| switch repoVars["type"] { |
| case "github": |
| gd := NewGithubDownloader() |
| |
| gd.Server = repoVars["server"] |
| gd.User = repoVars["user"] |
| gd.Repo = repoVars["repo"] |
| |
| // The project.yml file can contain github access tokens and |
| // authentication credentials, but this file is probably world-readable |
| // and therefore not a great place for this. |
| gd.Login = repoVars["login"] |
| gd.Password = repoVars["password"] |
| gd.PasswordEnv = repoVars["password_env"] |
| |
| // Alternatively, the user can put security material in |
| // $HOME/.newt/repos.yml. |
| newtrc := settings.Newtrc() |
| privRepo, err := newtrc.GetValStringMapString("repository."+repoName, nil) |
| util.OneTimeWarningError(err) |
| if privRepo != nil { |
| if gd.Login == "" { |
| gd.Login = privRepo["login"] |
| } |
| if gd.Password == "" { |
| gd.Password = privRepo["password"] |
| } |
| if gd.PasswordEnv == "" { |
| gd.PasswordEnv = privRepo["password_env"] |
| } |
| } |
| return gd, nil |
| |
| case "git": |
| gd := NewGitDownloader() |
| gd.Url = repoVars["url"] |
| if gd.Url == "" { |
| return nil, loadError("repo \"%s\" missing required field \"url\"", |
| repoName) |
| } |
| return gd, nil |
| |
| case "local": |
| ld := NewLocalDownloader() |
| ld.Path = repoVars["path"] |
| return ld, nil |
| |
| default: |
| return nil, loadError("invalid repository type: %s", repoVars["type"]) |
| } |
| } |