GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

working.go 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package git
  2. import (
  3. "context"
  4. "errors"
  5. "os"
  6. "path/filepath"
  7. )
  8. var (
  9. ErrReadOnly = errors.New("cannot make a working clone of a read-only git repo")
  10. )
  11. // Config holds some values we use when working in the working clone of
  12. // a repo.
  13. type Config struct {
  14. Branch string // branch we're syncing to
  15. Paths []string // paths within the repo containing files we care about
  16. NotesRef string
  17. UserName string
  18. UserEmail string
  19. SigningKey string
  20. SetAuthor bool
  21. SkipMessage string
  22. GitSecret bool
  23. }
  24. // Checkout is a local working clone of the remote repo. It is
  25. // intended to be used for one-off "transactions", e.g,. committing
  26. // changes then pushing upstream. It has no locking.
  27. type Checkout struct {
  28. dir string
  29. config Config
  30. upstream Remote
  31. realNotesRef string // cache the notes ref, since we use it to push as well
  32. }
  33. type Commit struct {
  34. Signature Signature
  35. Revision string
  36. Message string
  37. }
  38. // CommitAction is a struct holding commit information
  39. type CommitAction struct {
  40. Author string
  41. Message string
  42. SigningKey string
  43. }
  44. // TagAction is a struct holding tag parameters
  45. type TagAction struct {
  46. Tag string
  47. Revision string
  48. Message string
  49. SigningKey string
  50. }
  51. // Clone returns a local working clone of the sync'ed `*Repo`, using
  52. // the config given.
  53. func (r *Repo) Clone(ctx context.Context, conf Config) (*Checkout, error) {
  54. upstream := r.Origin()
  55. repoDir, err := r.workingClone(ctx, conf.Branch)
  56. if err != nil {
  57. return nil, err
  58. }
  59. if err := config(ctx, repoDir, conf.UserName, conf.UserEmail); err != nil {
  60. os.RemoveAll(repoDir)
  61. return nil, err
  62. }
  63. // We'll need the notes ref for pushing it, so make sure we have
  64. // it. This assumes we're syncing it (otherwise we'll likely get conflicts)
  65. realNotesRef, err := getNotesRef(ctx, repoDir, conf.NotesRef)
  66. if err != nil {
  67. os.RemoveAll(repoDir)
  68. return nil, err
  69. }
  70. r.mu.RLock()
  71. // Here is where we mimic `git fetch --tags --force`, but
  72. // _without_ overwriting head refs. This is only required for a
  73. // `Checkout` and _not_ for `Repo` as (bare) mirrors will happily
  74. // accept any ref changes to tags.
  75. //
  76. // NB: do this before any other fetch actions, as otherwise we may
  77. // get an 'existing tag clobber' error back.
  78. if err := fetch(ctx, repoDir, r.dir, `'+refs/tags/*:refs/tags/*'`); err != nil {
  79. os.RemoveAll(repoDir)
  80. r.mu.RUnlock()
  81. return nil, err
  82. }
  83. if err := fetch(ctx, repoDir, r.dir, realNotesRef+":"+realNotesRef); err != nil {
  84. os.RemoveAll(repoDir)
  85. r.mu.RUnlock()
  86. return nil, err
  87. }
  88. r.mu.RUnlock()
  89. if conf.GitSecret {
  90. if err := secretUnseal(ctx, repoDir); err != nil {
  91. return nil, err
  92. }
  93. }
  94. return &Checkout{
  95. dir: repoDir,
  96. upstream: upstream,
  97. realNotesRef: realNotesRef,
  98. config: conf,
  99. }, nil
  100. }
  101. // Clean a Checkout up (remove the clone)
  102. func (c *Checkout) Clean() {
  103. if c.dir != "" {
  104. os.RemoveAll(c.dir)
  105. }
  106. }
  107. // Dir returns the path to the repo
  108. func (c *Checkout) Dir() string {
  109. return c.dir
  110. }
  111. // ManifestDirs returns the paths to the manifests files. It ensures
  112. // that at least one path is returned, so that it can be used with
  113. // `Manifest.LoadManifests`.
  114. func (c *Checkout) ManifestDirs() []string {
  115. if len(c.config.Paths) == 0 {
  116. return []string{c.dir}
  117. }
  118. paths := make([]string, len(c.config.Paths), len(c.config.Paths))
  119. for i, p := range c.config.Paths {
  120. paths[i] = filepath.Join(c.dir, p)
  121. }
  122. return paths
  123. }
  124. // CommitAndPush commits changes made in this checkout, along with any
  125. // extra data as a note, and pushes the commit and note to the remote repo.
  126. func (c *Checkout) CommitAndPush(ctx context.Context, commitAction CommitAction, note interface{}, addUntracked bool) error {
  127. if addUntracked {
  128. if err := add(ctx, c.dir, "."); err != nil {
  129. return err
  130. }
  131. }
  132. if !check(ctx, c.dir, c.config.Paths, addUntracked) {
  133. return ErrNoChanges
  134. }
  135. commitAction.Message += c.config.SkipMessage
  136. if commitAction.SigningKey == "" {
  137. commitAction.SigningKey = c.config.SigningKey
  138. }
  139. if err := commit(ctx, c.dir, commitAction); err != nil {
  140. return err
  141. }
  142. if note != nil {
  143. rev, err := c.HeadRevision(ctx)
  144. if err != nil {
  145. return err
  146. }
  147. if err := addNote(ctx, c.dir, rev, c.config.NotesRef, note); err != nil {
  148. return err
  149. }
  150. }
  151. refs := []string{c.config.Branch}
  152. ok, err := refExists(ctx, c.dir, c.realNotesRef)
  153. if ok {
  154. refs = append(refs, c.realNotesRef)
  155. } else if err != nil {
  156. return err
  157. }
  158. if err := push(ctx, c.dir, c.upstream.URL, refs); err != nil {
  159. return PushError(c.upstream.URL, err)
  160. }
  161. return nil
  162. }
  163. // GetNote gets a note for the revision specified, or nil if there is no such note.
  164. func (c *Checkout) GetNote(ctx context.Context, rev string, note interface{}) (bool, error) {
  165. return getNote(ctx, c.dir, c.realNotesRef, rev, note)
  166. }
  167. func (c *Checkout) HeadRevision(ctx context.Context) (string, error) {
  168. return refRevision(ctx, c.dir, "HEAD")
  169. }
  170. func (c *Checkout) MoveTagAndPush(ctx context.Context, tagAction TagAction) error {
  171. if tagAction.SigningKey == "" {
  172. tagAction.SigningKey = c.config.SigningKey
  173. }
  174. return moveTagAndPush(ctx, c.dir, c.upstream.URL, tagAction)
  175. }
  176. // ChangedFiles does a git diff listing changed files
  177. func (c *Checkout) ChangedFiles(ctx context.Context, ref string) ([]string, error) {
  178. list, err := changed(ctx, c.dir, ref, c.config.Paths)
  179. if err == nil {
  180. for i, file := range list {
  181. list[i] = filepath.Join(c.dir, file)
  182. }
  183. }
  184. return list, err
  185. }
  186. func (c *Checkout) NoteRevList(ctx context.Context) (map[string]struct{}, error) {
  187. return noteRevList(ctx, c.dir, c.realNotesRef)
  188. }
  189. func (c *Checkout) Checkout(ctx context.Context, rev string) error {
  190. return checkout(ctx, c.dir, rev)
  191. }
  192. func (c *Checkout) Add(ctx context.Context, path string) error {
  193. return add(ctx, c.dir, path)
  194. }