| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package project |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "encoding/xml" |
| "errors" |
| "fmt" |
| "hash/fnv" |
| "io" |
| "io/ioutil" |
| "net/url" |
| "os" |
| "os/exec" |
| "path" |
| "path/filepath" |
| "regexp" |
| "sort" |
| "strings" |
| "text/template" |
| "time" |
| |
| "fuchsia.googlesource.com/jiri" |
| "fuchsia.googlesource.com/jiri/cipd" |
| "fuchsia.googlesource.com/jiri/envvar" |
| "fuchsia.googlesource.com/jiri/gerrit" |
| "fuchsia.googlesource.com/jiri/gitutil" |
| "fuchsia.googlesource.com/jiri/retry" |
| "golang.org/x/net/publicsuffix" |
| ) |
| |
| // Manifest represents a setting used for updating the universe. |
| type Manifest struct { |
| Version string `xml:"version,attr,omitempty"` |
| Attributes string `xml:"attributes,attr,omitempty"` |
| Imports []Import `xml:"imports>import"` |
| LocalImports []LocalImport `xml:"imports>localimport"` |
| Projects []Project `xml:"projects>project"` |
| ProjectOverrides []Project `xml:"overrides>project"` |
| ImportOverrides []Import `xml:"overrides>import"` |
| Hooks []Hook `xml:"hooks>hook"` |
| Packages []Package `xml:"packages>package"` |
| XMLName struct{} `xml:"manifest"` |
| } |
| |
| // ManifestFromBytes returns a manifest parsed from data, with defaults filled |
| // in. |
| func ManifestFromBytes(data []byte) (*Manifest, error) { |
| m := new(Manifest) |
| if len(data) > 0 { |
| if err := xml.Unmarshal(data, m); err != nil { |
| return nil, err |
| } |
| } |
| if err := m.fillDefaults(); err != nil { |
| return nil, err |
| } |
| return m, nil |
| } |
| |
| // ManifestFromFile returns a manifest parsed from the contents of filename, |
| // with defaults filled in. |
| // |
| // Note that unlike ProjectFromFile, ManifestFromFile does not convert project |
| // paths to absolute paths because it's possible to load a manifest with a |
| // specific root directory different from jirix.Root. The usual way to load a |
| // manifest is through LoadManifest, which does absolutize the paths, and uses |
| // the correct root directory. |
| func ManifestFromFile(jirix *jiri.X, filename string) (*Manifest, error) { |
| data, err := ioutil.ReadFile(filename) |
| if err != nil { |
| return nil, fmtError(err) |
| } |
| m, err := ManifestFromBytes(data) |
| if err != nil { |
| return nil, fmt.Errorf("invalid manifest %s: %v", filename, err) |
| } |
| return m, nil |
| } |
| |
| var ( |
| newlineBytes = []byte("\n") |
| emptyImportsBytes = []byte("\n <imports></imports>\n") |
| emptyProjectsBytes = []byte("\n <projects></projects>\n") |
| emptyOverridesBytes = []byte("\n <overrides></overrides>\n") |
| emptyHooksBytes = []byte("\n <hooks></hooks>\n") |
| emptyPackagesBytes = []byte("\n <packages></packages>\n") |
| |
| endElemBytes = []byte("/>\n") |
| endImportBytes = []byte("></import>\n") |
| endLocalImportBytes = []byte("></localimport>\n") |
| endProjectBytes = []byte("></project>\n") |
| endHookBytes = []byte("></hook>\n") |
| endPackageBytes = []byte("></package>\n") |
| |
| endImportSoloBytes = []byte("></import>") |
| endProjectSoloBytes = []byte("></project>") |
| endElemSoloBytes = []byte("/>") |
| |
| errGitHookNotRequired = errors.New("git hooks are not required") |
| ) |
| |
| const ( |
| fuchsiaGerritHost = "https://fuchsia-review.googlesource.com" |
| ) |
| |
| // deepCopy returns a deep copy of Manifest. |
| func (m *Manifest) deepCopy() *Manifest { |
| x := new(Manifest) |
| x.Imports = append([]Import(nil), m.Imports...) |
| x.LocalImports = append([]LocalImport(nil), m.LocalImports...) |
| x.Projects = append([]Project(nil), m.Projects...) |
| x.ProjectOverrides = append([]Project(nil), m.ProjectOverrides...) |
| x.ImportOverrides = append([]Import(nil), m.ImportOverrides...) |
| x.Hooks = append([]Hook(nil), m.Hooks...) |
| x.Packages = append([]Package(nil), m.Packages...) |
| x.Version = m.Version |
| x.Attributes = m.Attributes |
| return x |
| } |
| |
| // ToBytes returns m as serialized bytes, with defaults unfilled. |
| func (m *Manifest) ToBytes() ([]byte, error) { |
| m = m.deepCopy() // avoid changing manifest when unfilling defaults. |
| if err := m.unfillDefaults(); err != nil { |
| return nil, err |
| } |
| data, err := xml.MarshalIndent(m, "", " ") |
| if err != nil { |
| return nil, fmt.Errorf("manifest xml.Marshal failed: %v", err) |
| } |
| // It's hard (impossible?) to get xml.Marshal to elide some of the empty |
| // elements, or produce short empty elements, so we post-process the data. |
| data = bytes.Replace(data, emptyImportsBytes, newlineBytes, -1) |
| data = bytes.Replace(data, emptyProjectsBytes, newlineBytes, -1) |
| data = bytes.Replace(data, emptyOverridesBytes, newlineBytes, -1) |
| data = bytes.Replace(data, emptyHooksBytes, newlineBytes, -1) |
| data = bytes.Replace(data, emptyPackagesBytes, newlineBytes, -1) |
| data = bytes.Replace(data, endImportBytes, endElemBytes, -1) |
| data = bytes.Replace(data, endLocalImportBytes, endElemBytes, -1) |
| data = bytes.Replace(data, endProjectBytes, endElemBytes, -1) |
| data = bytes.Replace(data, endHookBytes, endElemBytes, -1) |
| data = bytes.Replace(data, endPackageBytes, endElemBytes, -1) |
| if !bytes.HasSuffix(data, newlineBytes) { |
| data = append(data, '\n') |
| } |
| return data, nil |
| } |
| |
| // ToFile writes the manifest m to a file with the given filename, with |
| // defaults unfilled and all project paths relative to the jiri root. |
| func (m *Manifest) ToFile(jirix *jiri.X, filename string) error { |
| // Replace absolute paths with relative paths to make it possible to move |
| // the root directory locally. |
| projects := []Project{} |
| for _, project := range m.Projects { |
| if err := project.relativizePaths(jirix.Root); err != nil { |
| return err |
| } |
| projects = append(projects, project) |
| } |
| // Sort the projects and hooks to ensure that the output of "jiri |
| // snapshot" is deterministic. Sorting the hooks by name allows |
| // some control over the ordering of the hooks in case that is |
| // necessary. |
| sort.Sort(ProjectsByPath(projects)) |
| m.Projects = projects |
| sort.Sort(HooksByName(m.Hooks)) |
| data, err := m.ToBytes() |
| if err != nil { |
| return err |
| } |
| return safeWriteFile(jirix, filename, data) |
| } |
| |
| func (m *Manifest) fillDefaults() error { |
| for index := range m.Imports { |
| if err := m.Imports[index].fillDefaults(); err != nil { |
| return err |
| } |
| } |
| for index := range m.LocalImports { |
| if err := m.LocalImports[index].validate(); err != nil { |
| return err |
| } |
| } |
| for index := range m.Projects { |
| if err := m.Projects[index].fillDefaults(); err != nil { |
| return err |
| } |
| } |
| for index := range m.ProjectOverrides { |
| if err := m.ProjectOverrides[index].fillDefaults(); err != nil { |
| return err |
| } |
| } |
| for index := range m.ImportOverrides { |
| if err := m.ImportOverrides[index].fillDefaults(); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func (m *Manifest) unfillDefaults() error { |
| for index := range m.Imports { |
| if err := m.Imports[index].unfillDefaults(); err != nil { |
| return err |
| } |
| } |
| for index := range m.LocalImports { |
| if err := m.LocalImports[index].validate(); err != nil { |
| return err |
| } |
| } |
| for index := range m.Projects { |
| if err := m.Projects[index].unfillDefaults(); err != nil { |
| return err |
| } |
| } |
| for index := range m.ProjectOverrides { |
| if err := m.ProjectOverrides[index].unfillDefaults(); err != nil { |
| return err |
| } |
| } |
| for index := range m.ImportOverrides { |
| if err := m.ImportOverrides[index].unfillDefaults(); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // Import represents a remote manifest import. |
| type Import struct { |
| // Manifest file to use from the remote manifest project. |
| Manifest string `xml:"manifest,attr,omitempty"` |
| // Name is the name of the remote manifest project, used to determine the |
| // project key. |
| Name string `xml:"name,attr,omitempty"` |
| // Remote is the remote manifest project to import. |
| Remote string `xml:"remote,attr,omitempty"` |
| // Revision is the revison to checkout, |
| // this takes precedence over RemoteBranch |
| Revision string `xml:"revision,attr,omitempty"` |
| // RemoteBranch is the name of the remote branch to track. |
| RemoteBranch string `xml:"remotebranch,attr,omitempty"` |
| // Root path, prepended to all project paths specified in the manifest file. |
| Root string `xml:"root,attr,omitempty"` |
| XMLName struct{} `xml:"import"` |
| } |
| |
| func (i *Import) fillDefaults() error { |
| if i.RemoteBranch == "" { |
| i.RemoteBranch = "master" |
| } |
| if i.Revision == "" { |
| i.Revision = "HEAD" |
| } |
| return i.validate() |
| } |
| |
| func (i *Import) RemoveDefaults() { |
| if i.RemoteBranch == "master" { |
| i.RemoteBranch = "" |
| } |
| if i.Revision == "HEAD" { |
| i.Revision = "" |
| } |
| } |
| |
| func (i *Import) unfillDefaults() error { |
| i.RemoveDefaults() |
| return i.validate() |
| } |
| |
| func (i *Import) validate() error { |
| if i.Manifest == "" || i.Remote == "" { |
| return fmt.Errorf("bad import: both manifest and remote must be specified") |
| } |
| return nil |
| } |
| |
| func (i *Import) toProject(path string) (Project, error) { |
| p := Project{ |
| Name: i.Name, |
| Path: path, |
| Remote: i.Remote, |
| Revision: i.Revision, |
| RemoteBranch: i.RemoteBranch, |
| } |
| err := p.fillDefaults() |
| return p, err |
| } |
| |
| // ProjectKey returns the unique ProjectKey for the imported project. |
| func (i *Import) ProjectKey() ProjectKey { |
| return MakeProjectKey(i.Name, i.Remote) |
| } |
| |
| // projectKeyFileName returns a file name based on the ProjectKey. |
| func (i *Import) projectKeyFileName() string { |
| // TODO(toddw): Disallow weird characters from project names. |
| hash := fnv.New64a() |
| hash.Write([]byte(i.ProjectKey())) |
| return fmt.Sprintf("%s_%x", i.Name, hash.Sum64()) |
| } |
| |
| // cycleKey returns a key based on the remote and manifest, used for |
| // cycle-detection. It's only valid for new-style remote imports; it's empty |
| // for the old-style local imports. |
| func (i *Import) cycleKey() string { |
| if i.Remote == "" { |
| return "" |
| } |
| // We don't join the remote and manifest with a slash or any other url-safe |
| // character, since that might not be unique. E.g. |
| // remote: https://foo.com/a/b remote: https://foo.com/a |
| // manifest: c manifest: b/c |
| // In both cases, the key would be https://foo.com/a/b/c. |
| return i.Remote + " + " + i.Manifest |
| } |
| |
| func (i *Import) update(o *Import) { |
| if o.Manifest != "" { |
| i.Manifest = o.Manifest |
| } |
| if o.Name != "" { |
| i.Name = o.Name |
| } |
| if o.Remote != "" { |
| i.Remote = o.Remote |
| } |
| if o.Revision != "" { |
| i.Revision = o.Revision |
| } |
| if o.RemoteBranch != "" { |
| i.RemoteBranch = o.RemoteBranch |
| } |
| if o.Root != "" { |
| i.Root = o.Root |
| } |
| } |
| |
| // LocalImport represents a local manifest import. |
| type LocalImport struct { |
| // Manifest file to import from. |
| File string `xml:"file,attr,omitempty"` |
| XMLName struct{} `xml:"localimport"` |
| } |
| |
| func (i *LocalImport) validate() error { |
| if i.File == "" { |
| return fmt.Errorf("bad localimport: must specify file: %+v", *i) |
| } |
| return nil |
| } |
| |
| type LocalConfig struct { |
| Ignore bool `xml:"ignore"` |
| NoUpdate bool `xml:"no-update"` |
| NoRebase bool `xml:"no-rebase"` |
| XMLName struct{} `xml:"config"` |
| } |
| |
| // Reads localConfig from given reader. Returns incorrect bytes |
| func (lc *LocalConfig) ReadFrom(r io.Reader) (int64, error) { |
| return 1, xml.NewDecoder(r).Decode(lc) |
| } |
| |
| func LocalConfigFromFile(jirix *jiri.X, filename string) (LocalConfig, error) { |
| var lc LocalConfig |
| f, err := os.Open(filename) |
| if os.IsNotExist(err) { |
| return lc, nil |
| } else if err != nil { |
| return lc, fmtError(err) |
| } |
| _, err = lc.ReadFrom(f) |
| return lc, err |
| } |
| |
| // Writes the localConfig to given writer. Returns incorrect bytes |
| func (lc *LocalConfig) WriteTo(writer io.Writer) (int64, error) { |
| encoder := xml.NewEncoder(writer) |
| encoder.Indent("", " ") |
| return 1, encoder.Encode(lc) |
| } |
| |
| func (lc *LocalConfig) ToFile(jirix *jiri.X, filename string) error { |
| if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { |
| return fmtError(err) |
| } |
| writer, err := os.Create(filename) |
| if err != nil { |
| return fmtError(err) |
| } |
| defer writer.Close() |
| _, err = lc.WriteTo(writer) |
| return err |
| } |
| |
| func WriteLocalConfig(jirix *jiri.X, project Project, lc LocalConfig) error { |
| configFile := filepath.Join(project.Path, jiri.ProjectMetaDir, jiri.ProjectConfigFile) |
| return lc.ToFile(jirix, configFile) |
| } |
| |
| // Hook represents a hook to run |
| type Hook struct { |
| Name string `xml:"name,attr"` |
| Action string `xml:"action,attr"` |
| ProjectName string `xml:"project,attr"` |
| XMLName struct{} `xml:"hook"` |
| ActionPath string `xml:"-"` |
| } |
| |
| // HookKey is a unique string for a project. |
| type HookKey string |
| |
| type Hooks map[HookKey]Hook |
| |
| // Key returns the unique HookKey for the hook. |
| func (h Hook) Key() HookKey { |
| return MakeHookKey(h.Name, h.ProjectName) |
| } |
| |
| // MakeHookKey returns the hook key, given the hook and project name. |
| func MakeHookKey(name, projectName string) HookKey { |
| return HookKey(name + KeySeparator + projectName) |
| } |
| |
| func (h *Hook) validate() error { |
| if strings.Contains(h.Name, KeySeparator) { |
| return fmt.Errorf("bad hook: name cannot contain %q: %+v", KeySeparator, *h) |
| } |
| if strings.Contains(h.ProjectName, KeySeparator) { |
| return fmt.Errorf("bad hook: project cannot contain %q: %+v", KeySeparator, *h) |
| } |
| return nil |
| } |
| |
| // HooksByName implements the Sort interface. It sorts Hooks by the Name |
| // and ProjectName field. |
| type HooksByName []Hook |
| |
| func (hooks HooksByName) Len() int { |
| return len(hooks) |
| } |
| func (hooks HooksByName) Swap(i, j int) { |
| hooks[i], hooks[j] = hooks[j], hooks[i] |
| } |
| func (hooks HooksByName) Less(i, j int) bool { |
| if hooks[i].Name == hooks[j].Name { |
| return hooks[i].ProjectName < hooks[j].ProjectName |
| } |
| return hooks[i].Name < hooks[j].Name |
| } |
| |
| // Package struct represents the <package> tag in manifest files. |
| type Package struct { |
| // Name represents the remote cipd path of the package. |
| Name string `xml:"name,attr"` |
| |
| // Version represents the version tag of the cipd package. |
| Version string `xml:"version,attr"` |
| |
| // Path stores the local path of fetched cipd package. |
| Path string `xml:"path,attr,omitempty"` |
| |
| // Internal marks if this package require special permission |
| // for access |
| Internal bool `xml:"internal,attr,omitempty"` |
| |
| // Platforms defines the available platforms for this cipd package. |
| Platforms string `xml:"platforms,attr,omitempty"` |
| |
| // Flag defines the content that should be written to a file when |
| // this package is successfully fetched. |
| Flag string `xml:"flag,attr,omitempty"` |
| |
| // Attributes store the the list attributes for this package. |
| // When it starts with "+", a computed default attributes will |
| // be appended. |
| Attributes string `xml:"attributes,attr,omitempty"` |
| |
| // Instances store the known instance ids for this package. |
| // It is mainly used by snapshot file. |
| Instances []PackageInstance `xml:"instance"` |
| XMLName struct{} `xml:"package"` |
| |
| // ComputedAttributes stores computed attributes object |
| // which is easiler to perform matching and comparing. |
| ComputedAttributes attributes `xml:"-"` |
| |
| // ManifestPath stores the absolute path of the manifest. |
| ManifestPath string `xml:"-"` |
| } |
| |
| type PackageKey string |
| |
| type Packages map[PackageKey]Package |
| |
| type PackageKeys []PackageKey |
| |
| func (p Package) Key() PackageKey { |
| return PackageKey(p.Path + KeySeparator + p.Name) |
| } |
| |
| func (pks PackageKeys) Len() int { return len(pks) } |
| func (pks PackageKeys) Less(i, j int) bool { return string(pks[i]) < string(pks[j]) } |
| func (pks PackageKeys) Swap(i, j int) { pks[i], pks[j] = pks[j], pks[i] } |
| |
| // FilterACL returns a new Packages map without any inaccessible packages. |
| func (p *Packages) FilterACL(jirix *jiri.X) (Packages, bool, error) { |
| // Perform ACL checks on internal projects |
| pkgACLMap := make(map[string]bool) |
| hasInternal := false |
| for _, pkg := range *p { |
| pkg.Name = strings.TrimRight(pkg.Name, "/") |
| if pkg.Internal { |
| hasInternal = true |
| pkgACLMap[pkg.Name] = false |
| } |
| } |
| if len(pkgACLMap) != 0 { |
| if err := cipd.CheckPackageACL(jirix, pkgACLMap); err != nil { |
| return nil, false, err |
| } |
| } |
| retPkgs := make(Packages) |
| for _, pkg := range *p { |
| if val, ok := pkgACLMap[pkg.Name]; ok && !val { |
| continue |
| } |
| retPkgs[pkg.Key()] = pkg |
| } |
| return retPkgs, hasInternal, nil |
| } |
| |
| type PackageInstance struct { |
| Name string `xml:"name,attr"` |
| ID string `xml:"id,attr"` |
| XMLName struct{} `xml:"instance"` |
| } |
| |
| // FillDefaults function fills default platforms information into |
| // Package struct if it is not defined and path is using template. |
| func (p *Package) FillDefaults() error { |
| if cipd.MustExpand(p.Name) && p.Platforms == "" { |
| for _, v := range cipd.DefaultPlatforms() { |
| p.Platforms += v.String() + "," |
| } |
| if p.Platforms[len(p.Platforms)-1] == ',' { |
| p.Platforms = p.Platforms[:len(p.Platforms)-1] |
| } |
| } |
| return nil |
| } |
| |
| // GetPath returns the relative path that Package p should be |
| // downloaded to. |
| func (p *Package) GetPath() (string, error) { |
| if p.Path == "" { |
| cipdPath := p.Name |
| // Replace template with current platform information. |
| // If failed, skip filling in default path. |
| if cipd.MustExpand(cipdPath) { |
| expanded, err := cipd.Expand(cipdPath, []cipd.Platform{cipd.CipdPlatform}) |
| if err != nil { |
| return "", err |
| } |
| if len(expanded) > 0 { |
| cipdPath = expanded[0] |
| } |
| } |
| if !cipd.MustExpand(cipdPath) { |
| base := path.Base(cipdPath) |
| if _, err := cipd.NewPlatform(base); err == nil { |
| // base is the name for a platform |
| base = filepath.Join(path.Base(path.Dir(cipdPath)), base) |
| } |
| return filepath.Join("prebuilt", base), nil |
| } |
| return "prebuilt", nil |
| } |
| return p.Path, nil |
| } |
| |
| // GetPlatforms returns the platforms information of |
| // this Package struct. |
| func (p *Package) GetPlatforms() ([]cipd.Platform, error) { |
| if err := p.FillDefaults(); err != nil { |
| return nil, err |
| } |
| retList := make([]cipd.Platform, 0) |
| platStrs := strings.Split(p.Platforms, ",") |
| for _, platStr := range platStrs { |
| if platStr == "" { |
| continue |
| } |
| plat, err := cipd.NewPlatform(platStr) |
| if err != nil { |
| return nil, err |
| } |
| retList = append(retList, plat) |
| } |
| return retList, nil |
| } |
| |
| // LoadManifest loads the manifest, starting with the .jiri_manifest file, |
| // resolving remote and local imports. Returns the projects specified by |
| // the manifest. |
| // |
| // WARNING: LoadManifest cannot be run multiple times in parallel! It invokes |
| // git operations which require a lock on the filesystem. If you see errors |
| // about ".git/index.lock exists", you are likely calling LoadManifest in |
| // parallel. |
| func LoadManifest(jirix *jiri.X) (Projects, Hooks, Packages, error) { |
| jirix.TimerPush("load manifest") |
| defer jirix.TimerPop() |
| file := jirix.JiriManifestFile() |
| localProjects, err := LocalProjects(jirix, FastScan) |
| if err != nil { |
| return nil, nil, nil, err |
| } |
| return LoadManifestFile(jirix, file, localProjects, false) |
| } |
| |
| func (ld *loader) warnOverrides(jirix *jiri.X) { |
| if len(ld.ProjectOverrides) != 0 { |
| for _, v := range ld.ProjectOverrides { |
| revision := v.Revision |
| if revision == "" { |
| revision = "HEAD" |
| } |
| jirix.Logger.Warningf("Project %s(remote: %s) is pinned to revision %s, if that is not what you want, please run \"jiri override --delete %s %s\" to unpin it.", v.Name, v.Remote, revision, v.Name, v.Remote) |
| jirix.OverrideWarned = true |
| } |
| } |
| if len(ld.ImportOverrides) != 0 { |
| for _, v := range ld.ImportOverrides { |
| revision := v.Revision |
| if revision == "" { |
| revision = "HEAD" |
| } |
| jirix.Logger.Warningf("Import %s(remote: %s) is pinned to revision %s, if that is not what you want, please run \"jiri override --import-manifest=%s --delete %s %s\" to unpin it.", v.Name, v.Remote, revision, v.Manifest, v.Name, v.Remote) |
| jirix.OverrideWarned = true |
| } |
| } |
| } |
| |
| func (ld *loader) enforceLocks(jirix *jiri.X) error { |
| enforceProjLocks := func(jirix *jiri.X) (err error) { |
| for _, v := range ld.Projects { |
| if projectLock, ok := ld.ProjectLocks[ProjectLockKey(v.Key())]; ok { |
| if v.Revision == "" || v.Revision == "HEAD" { |
| v.Revision = projectLock.Revision |
| ld.Projects[v.Key()] = v |
| } else if v.Revision != projectLock.Revision { |
| s := fmt.Sprintf("project %+v has conflicting revisions in manifest and jiri.lock: %s:%s", v, v.Revision, projectLock.Revision) |
| jirix.Logger.Debugf(s) |
| err = errors.New(s) |
| } |
| } |
| } |
| return |
| } |
| |
| enforcePkgLocks := func(jirix *jiri.X) (err error) { |
| usedPkgLocks := make(map[PackageLockKey]bool) |
| for k := range ld.PackageLocks { |
| usedPkgLocks[k] = false |
| } |
| for _, v := range ld.Packages { |
| plats, err := v.GetPlatforms() |
| if err != nil { |
| return err |
| } |
| pkgs, err := cipd.Expand(v.Name, plats) |
| if err != nil { |
| return err |
| } |
| for _, pkg := range pkgs { |
| if pkgLock, ok := ld.PackageLocks[PackageLockKey(pkg+KeySeparator+v.Version)]; ok { |
| if pkgLock.VersionTag != v.Version && !jirix.IgnoreLockConflicts { |
| // Package version conflicts detected. Treated it as an error. |
| s := fmt.Sprintf("package %q has conflicting version in manifest and jiri.lock: %s:%s", v.Name, v.Version, pkgLock.VersionTag) |
| jirix.Logger.Debugf(s) |
| err = errors.New(s) |
| } |
| ins := PackageInstance{ |
| Name: pkgLock.PackageName, |
| ID: pkgLock.InstanceID, |
| } |
| v.Instances = append(v.Instances, ins) |
| ld.Packages[v.Key()] = v |
| usedPkgLocks[pkgLock.Key()] = true |
| } else { |
| jirix.Logger.Debugf("Package %q is not found in jiri.lock", pkg) |
| } |
| } |
| if err != nil { |
| return err |
| } |
| } |
| for k, v := range usedPkgLocks { |
| if !v { |
| jirix.Logger.Debugf("PackageLock %v is not used", k) |
| } |
| } |
| return |
| } |
| |
| if err := enforceProjLocks(jirix); err != nil { |
| return err |
| } |
| |
| if err := enforcePkgLocks(jirix); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // LoadManifestFile loads the manifest starting with the given file, resolving |
| // remote and local imports. Local projects are used to resolve remote imports; |
| // if nil, encountering any remote import will result in an error. |
| // |
| // WARNING: LoadManifestFile cannot be run multiple times in parallel! It |
| // invokes git operations which require a lock on the filesystem. If you see |
| // errors about ".git/index.lock exists", you are likely calling |
| // LoadManifestFile in parallel. |
| func LoadManifestFile(jirix *jiri.X, file string, localProjects Projects, localManifest bool) (Projects, Hooks, Packages, error) { |
| ld := newManifestLoader(localProjects, false, file) |
| if err := ld.Load(jirix, "", "", file, "", "", "", localManifest); err != nil { |
| return nil, nil, nil, err |
| } |
| jirix.AddCleanupFunc(ld.cleanup) |
| if jirix.LockfileEnabled { |
| if err := ld.enforceLocks(jirix); err != nil { |
| return nil, nil, nil, err |
| } |
| } |
| if !jirix.OverrideWarned { |
| ld.warnOverrides(jirix) |
| } |
| ld.GenerateGitAttributesForProjects(jirix) |
| return ld.Projects, ld.Hooks, ld.Packages, nil |
| } |
| |
| // LoadUpdatedManifest loads an updated manifest starting with the .jiri_manifest file for localProjects. It will use |
| // local manifest files instead of manifest files in remote repositories if localManifest is set to true. |
| func LoadUpdatedManifest(jirix *jiri.X, localProjects Projects, localManifest bool) (Projects, Hooks, Packages, error) { |
| jirix.TimerPush("load updated manifest") |
| defer jirix.TimerPop() |
| ld := newManifestLoader(localProjects, true, jirix.JiriManifestFile()) |
| if err := ld.Load(jirix, "", "", jirix.JiriManifestFile(), "", "", "", localManifest); err != nil { |
| return nil, nil, nil, err |
| } |
| jirix.AddCleanupFunc(ld.cleanup) |
| if jirix.LockfileEnabled { |
| if err := ld.enforceLocks(jirix); err != nil { |
| return nil, nil, nil, err |
| } |
| } |
| if !jirix.OverrideWarned { |
| ld.warnOverrides(jirix) |
| } |
| ld.GenerateGitAttributesForProjects(jirix) |
| return ld.Projects, ld.Hooks, ld.Packages, nil |
| } |
| |
| // ResolveImplicitPackageVersions resolves the version field of packages if it |
| // pins to a project's revision hash |
| func ResolveImplicitPackageVersions(jirix *jiri.X, projects Projects, pkgs Packages) (Packages, error) { |
| // Example: |
| // <package name="fuchsia/dart-sdk/${platform}" |
| // path="third_party/dart/tools/sdks/dart-sdk" |
| // version="git_revision:{{(index .Projects "dart/sdk").Revision}}"/> |
| // The fuchsia/dart-sdk package is pinned to current dart/sdk project's Revision |
| templateRE := regexp.MustCompile(`{{[^}]*}}`) |
| var projMap struct { |
| Projects map[string]Project |
| } |
| retPkgs := make(Packages) |
| for k, v := range pkgs { |
| retPkgs[k] = v |
| } |
| projMap.Projects = make(map[string]Project) |
| for _, proj := range projects { |
| if v, ok := projMap.Projects[proj.Name]; ok { |
| // Just a warning since jiri could handle projects with duplicated names. |
| jirix.Logger.Warningf("Found more than 1 projects have the same name: %+v:%+v", proj, v) |
| } |
| projMap.Projects[proj.Name] = proj |
| } |
| |
| for _, pkg := range pkgs { |
| if !templateRE.MatchString(pkg.Version) { |
| continue |
| } |
| tpl, err := template.New("version").Parse(pkg.Version) |
| if err != nil { |
| return nil, err |
| } |
| var verBuf bytes.Buffer |
| if err := tpl.Execute(&verBuf, &projMap); err != nil { |
| return nil, err |
| } |
| pkg.Version = verBuf.String() |
| retPkgs[pkg.Key()] = pkg |
| } |
| return retPkgs, nil |
| } |
| |
| // resovlePackageLocks resolves instance ids using versions described in given |
| // pkgs using cipd. |
| func resolvePackageLocks(jirix *jiri.X, projects Projects, pkgs Packages) (PackageLocks, error) { |
| jirix.TimerPush("resolve instance id for cipd packages") |
| defer jirix.TimerPop() |
| |
| pkgs, _, err := pkgs.FilterACL(jirix) |
| if err != nil { |
| return nil, err |
| } |
| |
| ensureFilePath, err := generateEnsureFile(jirix, projects, pkgs, false) |
| if err != nil { |
| return nil, err |
| } |
| defer os.Remove(ensureFilePath) |
| |
| pkgInstances, err := cipd.Resolve(jirix, ensureFilePath) |
| if err != nil { |
| return nil, err |
| } |
| // TODO: Remove this boilerplate once we have a better package |
| // layout that doesn't cause import cycles |
| pkgLocks := make(PackageLocks) |
| for _, val := range pkgInstances { |
| pkgLock := PackageLock{ |
| PackageName: val.PackageName, |
| VersionTag: val.VersionTag, |
| InstanceID: val.InstanceID, |
| } |
| pkgLocks[pkgLock.Key()] = pkgLock |
| } |
| |
| return pkgLocks, nil |
| } |
| |
| // resolveProjectLocks resolves project revisions <project> tags in manifests |
| func resolveProjectLocks(jirix *jiri.X, projects Projects) (ProjectLocks, error) { |
| projectLocks := make(ProjectLocks) |
| for _, v := range projects { |
| projectLock := ProjectLock{v.Remote, v.Name, v.Revision} |
| projectLocks[projectLock.Key()] = projectLock |
| } |
| return projectLocks, nil |
| } |
| |
| // FetchPackages fetches prebuilt packages described in given pkgs using cipd. |
| // Parameter fetchTimeout is in minutes. |
| func FetchPackages(jirix *jiri.X, projects Projects, pkgs Packages, fetchTimeout uint) error { |
| jirix.TimerPush("fetch cipd packages") |
| defer jirix.TimerPop() |
| |
| pkgsWAccess, hasInternalPkgs, err := pkgs.FilterACL(jirix) |
| if err != nil { |
| return err |
| } |
| |
| ensureFilePath, err := generateEnsureFile(jirix, projects, pkgsWAccess, !jirix.LockfileEnabled || jirix.UsingSnapshot) |
| if err != nil { |
| return err |
| } |
| defer os.Remove(ensureFilePath) |
| |
| if jirix.LockfileEnabled && !jirix.UsingSnapshot { |
| versionFilePath, err := generateVersionFile(jirix, ensureFilePath, pkgs) |
| if err != nil { |
| return err |
| } |
| defer os.Remove(versionFilePath) |
| } |
| |
| if err := cipd.Ensure(jirix, ensureFilePath, jirix.Root, fetchTimeout); err != nil { |
| return err |
| } |
| |
| if hasInternalPkgs { |
| if err := writePackageJSON(jirix, len(pkgs) == len(pkgsWAccess)); err != nil { |
| return err |
| } |
| } |
| |
| // Write explict flags. |
| if err := WritePackageFlags(jirix, pkgs, pkgsWAccess); err != nil { |
| return err |
| } |
| |
| if len(pkgs) > len(pkgsWAccess) { |
| cipdLoggedIn, err := cipd.CheckLoggedIn(jirix) |
| if err != nil { |
| return err |
| } |
| if !cipdLoggedIn { |
| jirix.Logger.Warningf("Some packages are skipped by cipd due to lack of access, you might want to run \"%s auth-login\" and try again", jirix.CIPDPath()) |
| } |
| } |
| return writeAttributesJSON(jirix) |
| } |
| |
| // WritePackageFlags write flag files into project directory using in "flag" |
| // attribute from pkgs. |
| func WritePackageFlags(jirix *jiri.X, pkgs, pkgsWA Packages) error { |
| // The flag attribute has a format of $FILE_NAME|$FLAG_SUCCESSFUL|$FLAG_FAILED |
| // When a package is successfully downloaded, jiri will write $FLAG_SUCCESSFUL |
| // to $FILE_NAME. If the package is not downloaded due to access reasons, |
| // jiri will write $FLAG_FAILED to $FILE_NAME. |
| // '|' is a forbidden symbol in Windows path, which is unlikely |
| // to be used by path. |
| |
| flagMap := make(map[string]string) |
| fill := func(file, flag string) error { |
| if v, ok := flagMap[file]; ok { |
| if v != flag { |
| return fmt.Errorf("encountered conflicting flags for file %q: %q conflicts with %q", file, v, flag) |
| } |
| } else { |
| flagMap[file] = flag |
| } |
| return nil |
| } |
| |
| for k, v := range pkgs { |
| if v.Flag == "" { |
| continue |
| } |
| fields := strings.Split(v.Flag, "|") |
| if len(fields) != 3 { |
| return fmt.Errorf("unknown package flag format found in package %+v", v) |
| } |
| if _, ok := pkgsWA[k]; ok { |
| // package is successfully fetched, write successful flag. |
| if err := fill(fields[0], fields[1]); err != nil { |
| return err |
| } |
| } else { |
| // package is failed to fetched, write failed flag. |
| if err := fill(fields[0], fields[2]); err != nil { |
| return err |
| } |
| } |
| } |
| |
| var writeErrorBuf bytes.Buffer |
| for k, v := range flagMap { |
| if err := ioutil.WriteFile(filepath.Join(jirix.Root, k), []byte(v), 0644); err != nil { |
| writeErrorBuf.WriteString(fmt.Sprintf("write package flag %q to file %q failed due to error: %v\n", v, k, err)) |
| } |
| } |
| if writeErrorBuf.Len() > 0 { |
| return errors.New(writeErrorBuf.String()) |
| } |
| return nil |
| } |
| |
| // GenerateJSON generates a json file which contains fetched |
| // packages. |
| func writePackageJSON(jirix *jiri.X, access bool) error { |
| var internalAccess struct { |
| Access bool `json:"internal_access"` |
| } |
| internalAccess.Access = access |
| jsonData, err := json.MarshalIndent(&internalAccess, "", " ") |
| if err != nil { |
| return err |
| } |
| if jirix.PrebuiltJSON == "" { |
| // Skip json file creation if PrebuiltJSON is not set. |
| return nil |
| } |
| return ioutil.WriteFile(filepath.Join(jirix.RootMetaDir(), jirix.PrebuiltJSON), jsonData, 0644) |
| } |
| |
| func generateEnsureFile(jirix *jiri.X, projects Projects, pkgs Packages, ignoreCryptoCheck bool) (string, error) { |
| pkgs, err := ResolveImplicitPackageVersions(jirix, projects, pkgs) |
| if err != nil { |
| return "", err |
| } |
| ensureFile, err := ioutil.TempFile("", "jiri*.ensure") |
| if err != nil { |
| return "", fmt.Errorf("not able to create tmp file: %v", err) |
| } |
| defer ensureFile.Close() |
| ensureFilePath := ensureFile.Name() |
| |
| // Write header information |
| // TODO: add "verfy_platform" attribute to each package tag |
| // to avoid hardcoding platform names in Jiri |
| var ensureFileBuf bytes.Buffer |
| if !ignoreCryptoCheck { |
| // Collect platforms used by this project |
| allPlats := make(map[string]cipd.Platform) |
| // CIPD ensure-file-resolve requires $VerifiedPlatform to be present |
| // even if the package name is not using ${platform} template. |
| // Put DefaultPlatforms into header to walkaround this issue. |
| for _, plat := range cipd.DefaultPlatforms() { |
| allPlats[plat.String()] = plat |
| } |
| for _, pkg := range pkgs { |
| plats, err := pkg.GetPlatforms() |
| if err != nil { |
| return "", err |
| } |
| for _, plat := range plats { |
| allPlats[plat.String()] = plat |
| } |
| } |
| |
| for _, plat := range allPlats { |
| ensureFileBuf.WriteString(fmt.Sprintf("$VerifiedPlatform %s\n", plat)) |
| } |
| versionFileName := ensureFilePath[:len(ensureFilePath)-len(".ensure")] + ".version" |
| ensureFileBuf.WriteString("$ResolvedVersions " + versionFileName + "\n") |
| } |
| if jirix.CipdParanoidMode { |
| ensureFileBuf.WriteString("$ParanoidMode CheckPresence\n") |
| } |
| ensureFileBuf.WriteString("\n") |
| |
| for _, pkg := range pkgs { |
| |
| cipdDecl, err := pkg.cipdDecl(jirix) |
| if err != nil { |
| return "", err |
| } |
| ensureFileBuf.WriteString(cipdDecl) |
| ensureFileBuf.WriteString("\n") |
| } |
| |
| jirix.Logger.Debugf("Generated ensure file content:\n%v", ensureFileBuf.String()) |
| if _, err := ensureFileBuf.WriteTo(ensureFile); err != nil { |
| return "", err |
| } |
| if err := ensureFile.Sync(); err != nil { |
| return "", err |
| } |
| |
| return ensureFilePath, nil |
| } |
| |
| func (p *Package) cipdDecl(jirix *jiri.X) (string, error) { |
| var buf bytes.Buffer |
| // Write "@Subdir" line to cipd declaration |
| subdir, err := p.GetPath() |
| if err != nil { |
| return "", err |
| } |
| tmpl, err := template.New("pack").Parse(subdir) |
| if err != nil { |
| return "", fmt.Errorf("parsing package path %q failed", subdir) |
| } |
| var subdirBuf bytes.Buffer |
| // subdir is using fuchsia platform format instead of |
| // using cipd platform format |
| tmpl.Execute(&subdirBuf, cipd.FuchsiaPlatform(cipd.CipdPlatform)) |
| subdir = subdirBuf.String() |
| buf.WriteString(fmt.Sprintf("@Subdir %s\n", subdir)) |
| // Write package version line to cipd declaration |
| plats, err := p.GetPlatforms() |
| if err != nil { |
| return "", err |
| } |
| var cipdPath, version string |
| version = p.Version |
| cipdPath, err = cipd.Decl(p.Name, plats) |
| if err != nil { |
| return "", err |
| } |
| if jirix.UsingSnapshot && len(p.Instances) != 0 { |
| candPath, err := cipd.Expand(p.Name, []cipd.Platform{cipd.CipdPlatform}) |
| if err != nil { |
| return "", err |
| } |
| if len(candPath) > 0 { |
| for _, inst := range p.Instances { |
| if inst.Name == candPath[0] { |
| cipdPath = candPath[0] |
| version = inst.ID |
| break |
| } |
| } |
| } else { |
| // cipd.Expand failed expand |
| // This may happen if the cipdPath does not allow platform |
| // in cipd.CipdPlatform. E.g. |
| // "example/linux-${arch=amd64}" expanded with "linux-arm64". |
| // Leave a log in Debug log. |
| jirix.Logger.Debugf("cipd.Expand failed to expand cipd path %q using platforms %v", p.Name, cipd.CipdPlatform) |
| } |
| } |
| buf.WriteString(fmt.Sprintf("%s %s\n", cipdPath, version)) |
| return buf.String(), nil |
| } |
| |
| func generateVersionFile(jirix *jiri.X, ensureFile string, pkgs Packages) (string, error) { |
| versionFileName := ensureFile[:len(ensureFile)-len(".ensure")] + ".version" |
| |
| var versionFileBuf bytes.Buffer |
| // Just pour everything in pkgLocks into version file without matching package |
| // names. cipd will do the matching for us. |
| for _, pkg := range pkgs { |
| jirix.Logger.Debugf("Generate version file using %+v", pkg) |
| for _, ins := range pkg.Instances { |
| decl := fmt.Sprintf("\n%s\n\t%s\n\t%s\n", ins.Name, pkg.Version, ins.ID) |
| versionFileBuf.WriteString(decl) |
| } |
| } |
| jirix.Logger.Debugf("Generated version file content:\n%v", versionFileBuf.String()) |
| return versionFileName, ioutil.WriteFile(versionFileName, versionFileBuf.Bytes(), 0655) |
| } |
| |
| // RunHooks runs all given hooks. |
| func RunHooks(jirix *jiri.X, hooks Hooks, runHookTimeout uint) error { |
| jirix.TimerPush("run hooks") |
| defer jirix.TimerPop() |
| jirix.Logger.Debugf("Running Jiri hooks") |
| defer jirix.Logger.Debugf("Running Jiri ") |
| type result struct { |
| outFile *os.File |
| errFile *os.File |
| err error |
| } |
| ch := make(chan result) |
| tmpDir, err := ioutil.TempDir("", "run-hooks") |
| if err != nil { |
| return fmt.Errorf("not able to create tmp dir: %v", err) |
| } |
| defer os.RemoveAll(tmpDir) |
| for _, hook := range hooks { |
| go func(hook Hook) { |
| logStr := fmt.Sprintf("running hook(%s) for project %q", hook.Name, hook.ProjectName) |
| jirix.Logger.Debugf(logStr) |
| task := jirix.Logger.AddTaskMsg(logStr) |
| defer task.Done() |
| outFile, err := ioutil.TempFile(tmpDir, hook.Name+"-out") |
| if err != nil { |
| ch <- result{nil, nil, fmtError(err)} |
| return |
| } |
| errFile, err := ioutil.TempFile(tmpDir, hook.Name+"-err") |
| if err != nil { |
| ch <- result{nil, nil, fmtError(err)} |
| return |
| } |
| |
| fmt.Fprintf(outFile, "output for hook(%v) for project %q\n", hook.Name, hook.ProjectName) |
| fmt.Fprintf(errFile, "Error for hook(%v) for project %q\n", hook.Name, hook.ProjectName) |
| cmdLine := filepath.Join(hook.ActionPath, hook.Action) |
| err = retry.Function(jirix, func() error { |
| ctx, cancel := context.WithTimeout(context.Background(), time.Duration(runHookTimeout)*time.Minute) |
| defer cancel() |
| command := exec.CommandContext(ctx, cmdLine) |
| command.Dir = hook.ActionPath |
| command.Stdin = os.Stdin |
| command.Stdout = outFile |
| command.Stderr = errFile |
| env := jirix.Env() |
| command.Env = envvar.MapToSlice(env) |
| jirix.Logger.Tracef("Run: %q", cmdLine) |
| err = command.Run() |
| if ctx.Err() == context.DeadlineExceeded { |
| err = ctx.Err() |
| } |
| scm := gitutil.New(jirix, gitutil.RootDirOpt(filepath.Dir(filepath.Dir(cmdLine)))) |
| revision, err2 := scm.CurrentRevisionOfBranch("HEAD") |
| if err2 == nil { |
| jirix.Logger.Debugf(" Invoked hook(%v) for project %q on revision %q", hook.Name, hook.ProjectName, revision) |
| } |
| return err |
| }, fmt.Sprintf("running hook(%s) for project %s", hook.Name, hook.ProjectName), |
| retry.AttemptsOpt(jirix.Attempts)) |
| ch <- result{outFile, errFile, err} |
| }(hook) |
| |
| } |
| |
| err = nil |
| timeout := false |
| for range hooks { |
| out := <-ch |
| defer func() { |
| if out.outFile != nil { |
| out.outFile.Close() |
| } |
| if out.errFile != nil { |
| out.errFile.Close() |
| } |
| }() |
| if out.err == context.DeadlineExceeded { |
| timeout = true |
| out.outFile.Sync() |
| out.outFile.Seek(0, 0) |
| var buf bytes.Buffer |
| io.Copy(&buf, out.outFile) |
| jirix.Logger.Errorf("Timeout while executing hook\n%s\n\n", buf.String()) |
| err = fmt.Errorf("Hooks execution failed.") |
| continue |
| } |
| var outBuf bytes.Buffer |
| if out.outFile != nil { |
| out.outFile.Sync() |
| out.outFile.Seek(0, 0) |
| io.Copy(&outBuf, out.outFile) |
| } |
| if out.err != nil { |
| var buf bytes.Buffer |
| if out.errFile != nil { |
| out.errFile.Sync() |
| out.errFile.Seek(0, 0) |
| io.Copy(&buf, out.errFile) |
| } |
| jirix.Logger.Errorf("%s\n%s\n%s\n", out.err, buf.String(), outBuf.String()) |
| err = fmt.Errorf("Hooks execution failed.") |
| } else { |
| if outBuf.String() != "" { |
| jirix.Logger.Debugf("%s\n", outBuf.String()) |
| } |
| } |
| } |
| if timeout { |
| err = fmt.Errorf("%s Use %s flag to set timeout.", err, jirix.Color.Yellow("-hook-timeout")) |
| } |
| return err |
| } |
| |
| type commitMsgFetcher map[string][]byte |
| |
| func (f commitMsgFetcher) fetch(jirix *jiri.X, gerritHost, path string) ([]byte, error) { |
| bytes, ok := f[gerritHost] |
| if !ok { |
| jirix.Logger.Debugf("Fetching %q", gerritHost+"/tools/hooks/commit-msg") |
| data, err := gerrit.FetchFile(gerritHost, "/tools/hooks/commit-msg") |
| if err != nil { |
| if err != gerrit.ErrRedirectOnGerrit { |
| // Network or disk IO error, halt jiri |
| return nil, err |
| } |
| // gerritHost require SSO login |
| if jirix.RewriteSsoToHttps { |
| // Gerrit host require SSO but jiri has rewritesso flag turned on |
| // In this case git hooks are useless, stop fetching git hooks |
| return nil, errGitHookNotRequired |
| } |
| |
| // Use commit-msg in cache if the domain has same eTLD and SLD. |
| for k, v := range f { |
| urlK, err := url.Parse(k) |
| if err != nil { |
| // This should not happen as this url is already downloaded before. |
| return nil, fmt.Errorf("download commit-msg hook for host %q failed due to error %v", gerritHost, err) |
| } |
| urlG, err := url.Parse(gerritHost) |
| if err != nil { |
| // This should not happen either as gerritHost will be parsed by gerrit.FetchFile |
| return nil, fmt.Errorf("download commit-msg hook from host %q failed due to error %v", gerritHost, err) |
| } |
| etpoK, err := publicsuffix.EffectiveTLDPlusOne(urlK.Hostname()) |
| if err != nil { |
| // This should not happen as Both SLD and TLD should exist. |
| return nil, fmt.Errorf("download commit-msg hook from host %q failed due to error %v", gerritHost, err) |
| } |
| etpoG, err := publicsuffix.EffectiveTLDPlusOne(urlG.Hostname()) |
| if err != nil { |
| // This should not happen as Both SLD and TLD should exist. |
| return nil, fmt.Errorf("download commit-msg hook from host %q failed due to error %v", gerritHost, err) |
| } |
| |
| if etpoK == etpoG { |
| jirix.Logger.Debugf("use commit-msg hook from host %q for host %q due to access limitations", k, gerritHost) |
| data = v |
| err = nil |
| break |
| } |
| } |
| |
| if data == nil { |
| // Could not find commit-msg in cache from domains with same eTLD and SLD. |
| // Fetch commit-msg from fuchsia's gerrit server. |
| data, err = gerrit.FetchFile(fuchsiaGerritHost, "/tools/hooks/commit-msg") |
| if err != nil { |
| // This will only happen if configuration error occured on fuchsia gerrit server |
| return nil, fmt.Errorf("download commit-msg hook from host %q failed due to error %v", fuchsiaGerritHost, err) |
| } |
| jirix.Logger.Debugf("fallback to commit-msg from host %q for host %q due to access limitations", fuchsiaGerritHost, gerritHost) |
| } |
| } |
| f[gerritHost] = data |
| return data, nil |
| } |
| jirix.Logger.Debugf("Cached %q", gerritHost+"/tools/hooks/commit-msg") |
| return bytes, nil |
| } |
| |
| func applyGitHooks(jirix *jiri.X, ops []operation) error { |
| jirix.TimerPush("apply githooks") |
| defer jirix.TimerPop() |
| commitMsgFetcher := commitMsgFetcher{} |
| for _, op := range ops { |
| if op.Kind() != "delete" && !op.Project().LocalConfig.Ignore && !op.Project().LocalConfig.NoUpdate { |
| if op.Project().GerritHost != "" { |
| hookPath := filepath.Join(op.Project().Path, ".git", "hooks", "commit-msg") |
| commitHook, err := os.Create(hookPath) |
| if err != nil { |
| return fmtError(err) |
| } |
| bytes, err := commitMsgFetcher.fetch(jirix, op.Project().GerritHost, "/tools/hooks/commit-msg") |
| if err != nil { |
| if err != errGitHookNotRequired { |
| jirix.Logger.Debugf("%v", err) |
| } |
| commitHook.Close() |
| os.Remove(hookPath) |
| continue |
| } |
| |
| if _, err := commitHook.Write(bytes); err != nil { |
| return err |
| } |
| jirix.Logger.Debugf("Saved commit-msg hook to project %q", op.Project().Path) |
| commitHook.Close() |
| if err := os.Chmod(hookPath, 0750); err != nil { |
| return fmtError(err) |
| } |
| } |
| hookPath := filepath.Join(op.Project().Path, ".git", "hooks", "post-commit") |
| commitHook, err := os.Create(hookPath) |
| if err != nil { |
| return err |
| } |
| bytes := []byte(`#!/bin/sh |
| |
| if ! git symbolic-ref HEAD &> /dev/null; then |
| echo -e "WARNING: You are in a detached head state! You might lose this commit.\nUse 'git checkout -b <branch> to put it on a branch.\n" |
| fi |
| `) |
| if _, err := commitHook.Write(bytes); err != nil { |
| return err |
| } |
| commitHook.Close() |
| if err := os.Chmod(hookPath, 0750); err != nil { |
| return err |
| } |
| } |
| if op.Project().GitHooks == "" { |
| continue |
| } |
| // Don't want to run hooks when repo is deleted |
| if op.Kind() == "delete" { |
| continue |
| } |
| // Apply git hooks, overwriting any existing hooks. Jiri is in control of |
| // writing all hooks. |
| gitHooksDstDir := filepath.Join(op.Project().Path, ".git", "hooks") |
| // Copy the specified GitHooks directory into the project's git |
| // hook directory. We walk the file system, creating directories |
| // and copying files as we encounter them. |
| copyFn := func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| relPath, err := filepath.Rel(op.Project().GitHooks, path) |
| if err != nil { |
| return err |
| } |
| dst := filepath.Join(gitHooksDstDir, relPath) |
| if info.IsDir() { |
| return fmtError(os.MkdirAll(dst, 0755)) |
| } |
| src, err := ioutil.ReadFile(path) |
| if err != nil { |
| return fmtError(err) |
| } |
| // The file *must* be executable to be picked up by git. |
| return fmtError(ioutil.WriteFile(dst, src, 0755)) |
| } |
| if err := filepath.Walk(op.Project().GitHooks, copyFn); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |