mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-06 07:56:55 +08:00
Initial commit, pt. 23
This commit is contained in:
2
go.mod
2
go.mod
@@ -37,6 +37,7 @@ require (
|
|||||||
github.com/go-text/render v0.1.0 // indirect
|
github.com/go-text/render v0.1.0 // indirect
|
||||||
github.com/go-text/typesetting v0.1.0 // indirect
|
github.com/go-text/typesetting v0.1.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||||
github.com/gorilla/websocket v1.5.2 // indirect
|
github.com/gorilla/websocket v1.5.2 // indirect
|
||||||
github.com/hashicorp/logutils v1.0.0 // indirect
|
github.com/hashicorp/logutils v1.0.0 // indirect
|
||||||
@@ -85,6 +86,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
|
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/google/go-github/v62 v62.0.0
|
||||||
github.com/google/s2a-go v0.1.7 // indirect
|
github.com/google/s2a-go v0.1.7 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||||
|
4
go.sum
4
go.sum
@@ -224,6 +224,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
|
||||||
|
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
|
||||||
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
437
pkg/repository/git.go
Normal file
437
pkg/repository/git.go
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/facebookincubator/go-belt/tool/logger"
|
||||||
|
"github.com/go-git/go-billy/v5/memfs"
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||||
|
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||||
|
"github.com/go-git/go-git/v5/storage/memory"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GIT struct {
|
||||||
|
RemoteURL string
|
||||||
|
PrivateKey []byte
|
||||||
|
Auth transport.AuthMethod
|
||||||
|
Repo *git.Repository
|
||||||
|
FilePath string
|
||||||
|
CommitterName string
|
||||||
|
CommitterEmail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGit(
|
||||||
|
ctx context.Context,
|
||||||
|
remoteURL string,
|
||||||
|
privateKey []byte,
|
||||||
|
filePath string,
|
||||||
|
committerName string,
|
||||||
|
committerEmail string,
|
||||||
|
) (*GIT, error) {
|
||||||
|
stor := &GIT{
|
||||||
|
RemoteURL: remoteURL,
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
FilePath: filePath,
|
||||||
|
CommitterName: committerName,
|
||||||
|
CommitterEmail: committerEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := stor.init(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) init(ctx context.Context) error {
|
||||||
|
if len(g.RemoteURL) == 0 {
|
||||||
|
return fmt.Errorf("repo URL is not provided")
|
||||||
|
}
|
||||||
|
if len(g.PrivateKey) == 0 {
|
||||||
|
return fmt.Errorf("key is not provided")
|
||||||
|
}
|
||||||
|
auth, err := gitssh.NewPublicKeys("git", g.PrivateKey, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create auth object for git: %w", err)
|
||||||
|
}
|
||||||
|
logger.Tracef(ctx, "auth: %#+v", auth)
|
||||||
|
auth.HostKeyCallbackHelper.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
g.Auth = auth
|
||||||
|
|
||||||
|
repo, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
|
||||||
|
URL: g.RemoteURL,
|
||||||
|
Auth: g.Auth,
|
||||||
|
SingleBranch: true,
|
||||||
|
Mirror: true,
|
||||||
|
NoCheckout: true,
|
||||||
|
Depth: 0,
|
||||||
|
RecurseSubmodules: 0,
|
||||||
|
Progress: nil,
|
||||||
|
Tags: 0,
|
||||||
|
})
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
case plumbing.ErrReferenceNotFound, transport.ErrEmptyRemoteRepository:
|
||||||
|
repo, err = g.initGitRepo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to initialize the git repo: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unable to clone the git repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Repo = repo
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrNeedsRebase struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrNeedsRebase) Error() string {
|
||||||
|
return fmt.Sprintf("needs rebase: %v", err.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrNeedsRebase) Unwrap() error {
|
||||||
|
return err.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) initGitRepo(ctx context.Context) (*git.Repository, error) {
|
||||||
|
repo, err := git.Init(memory.NewStorage(), memfs.New())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to initialize the git local repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.initLocalBranch(ctx, repo); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to make a local branch: %w", err)
|
||||||
|
}
|
||||||
|
if err := g.push(ctx, repo); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to push the branch to the remote: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) initLocalBranch(
|
||||||
|
_ context.Context,
|
||||||
|
repo *git.Repository,
|
||||||
|
) error {
|
||||||
|
worktree, err := repo.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get git's worktree: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
signature := g.gitCommitter(now)
|
||||||
|
|
||||||
|
_, err = worktree.Commit("init repo", &git.CommitOptions{
|
||||||
|
All: false,
|
||||||
|
AllowEmptyCommits: true,
|
||||||
|
Author: signature,
|
||||||
|
Committer: signature,
|
||||||
|
Amend: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the first commit in the git repo: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) push(
|
||||||
|
ctx context.Context,
|
||||||
|
repo interface {
|
||||||
|
PushContext(context.Context, *git.PushOptions) error
|
||||||
|
},
|
||||||
|
) error {
|
||||||
|
err := repo.PushContext(ctx, &git.PushOptions{
|
||||||
|
RemoteURL: g.RemoteURL,
|
||||||
|
Auth: g.Auth,
|
||||||
|
Progress: nil,
|
||||||
|
Prune: false,
|
||||||
|
Force: false,
|
||||||
|
InsecureSkipTLS: false,
|
||||||
|
CABundle: nil,
|
||||||
|
RequireRemoteRefs: nil,
|
||||||
|
FollowTags: false,
|
||||||
|
ForceWithLease: nil,
|
||||||
|
Options: nil,
|
||||||
|
Atomic: false,
|
||||||
|
})
|
||||||
|
logger.Debugf(ctx, "push result: %v", err)
|
||||||
|
if err == git.NoErrAlreadyUpToDate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil && strings.Contains(err.Error(), "is at") && strings.Contains(err.Error(), "but expected") {
|
||||||
|
return ErrNeedsRebase{Err: err}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) Read() ([]byte, error) {
|
||||||
|
worktree, err := g.Repo.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to open the git's worktree: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := worktree.Filesystem.Open(g.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to open '%s': %w", g.FilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to read the content of file '%s' from the virtual git repository: %w", g.FilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) Pull(
|
||||||
|
ctx context.Context,
|
||||||
|
lastKnownCommitHash plumbing.Hash,
|
||||||
|
onUpdate func(
|
||||||
|
ctx context.Context,
|
||||||
|
commitHash plumbing.Hash,
|
||||||
|
newData []byte,
|
||||||
|
),
|
||||||
|
) (_err error) {
|
||||||
|
logger.Debugf(ctx, "gitStorage.Pull")
|
||||||
|
defer func() { logger.Debugf(ctx, "/gitStorage.Pull: %v", _err) }()
|
||||||
|
|
||||||
|
err := g.Repo.FetchContext(ctx, &git.FetchOptions{
|
||||||
|
RemoteURL: g.RemoteURL,
|
||||||
|
Auth: g.Auth,
|
||||||
|
Force: true,
|
||||||
|
Prune: false,
|
||||||
|
})
|
||||||
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
||||||
|
return fmt.Errorf("unable to fetch from the remote git repo: %w", err)
|
||||||
|
}
|
||||||
|
worktree, err := g.Repo.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open the git's worktree: %w", err)
|
||||||
|
}
|
||||||
|
if err != git.NoErrAlreadyUpToDate {
|
||||||
|
err = worktree.PullContext(ctx, &git.PullOptions{
|
||||||
|
RemoteURL: g.RemoteURL,
|
||||||
|
SingleBranch: true,
|
||||||
|
Depth: 0,
|
||||||
|
Auth: g.Auth,
|
||||||
|
RecurseSubmodules: 0,
|
||||||
|
Progress: nil,
|
||||||
|
Force: true,
|
||||||
|
InsecureSkipTLS: false,
|
||||||
|
CABundle: nil,
|
||||||
|
ProxyOptions: transport.ProxyOptions{},
|
||||||
|
})
|
||||||
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
||||||
|
return fmt.Errorf("unable to pull the updates in the git repo: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := g.Repo.Head()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the current git ref: %w", err)
|
||||||
|
}
|
||||||
|
newCommitHash := ref.Hash()
|
||||||
|
|
||||||
|
err = worktree.Checkout(&git.CheckoutOptions{
|
||||||
|
Hash: newCommitHash,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to checkout HEAD: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newData, err := g.Read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse config from the git source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastKnownCommitHash == newCommitHash {
|
||||||
|
logger.Debugf(ctx, "git is already in sync: %s == %s", lastKnownCommitHash, newCommitHash)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logger.Debugf(ctx, "got a different commit from git: %s != %s", lastKnownCommitHash, newCommitHash)
|
||||||
|
|
||||||
|
oldCommit, _ := g.Repo.CommitObject(newCommitHash)
|
||||||
|
if oldCommit != nil {
|
||||||
|
logger.Debugf(ctx, "we already have this commit in the history on our side, skipping it: %s: %#+v", newCommitHash, oldCommit)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(ctx, newCommitHash, newData)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) gitCommitter(now time.Time) *object.Signature {
|
||||||
|
return &object.Signature{
|
||||||
|
Name: g.CommitterName,
|
||||||
|
Email: g.CommitterEmail,
|
||||||
|
When: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) CommitAndPush(
|
||||||
|
ctx context.Context,
|
||||||
|
worktree *git.Worktree,
|
||||||
|
ref *plumbing.Reference,
|
||||||
|
) (plumbing.Hash, error) {
|
||||||
|
logger.Debugf(ctx, "gitStorage.CommitAndPush")
|
||||||
|
defer logger.Debugf(ctx, "/gitStorage.CommitAndPush")
|
||||||
|
|
||||||
|
_, err := worktree.Add(g.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to add file '%s' to the git's worktree: %w", g.FilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
signature := g.gitCommitter(now)
|
||||||
|
ts := now.Format(time.DateTime)
|
||||||
|
host, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to determine the host name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := worktree.Commit(fmt.Sprintf("Update from '%s' at %s", host, ts), &git.CommitOptions{
|
||||||
|
All: true,
|
||||||
|
AllowEmptyCommits: false,
|
||||||
|
Author: signature,
|
||||||
|
Committer: signature,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return hash, fmt.Errorf("unable to commit the new config to the git repo: %w", err)
|
||||||
|
}
|
||||||
|
logger.Debugf(ctx, "new commit: %s", hash)
|
||||||
|
|
||||||
|
newRef := plumbing.NewHashReference(ref.Name(), hash)
|
||||||
|
err = g.Repo.Storer.SetReference(newRef)
|
||||||
|
if err != nil {
|
||||||
|
return hash, fmt.Errorf("unable to set git reference %#+v: %w", *newRef, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf(ctx, "had set reference: %#+v", *newRef)
|
||||||
|
|
||||||
|
err = g.push(ctx, g.Repo)
|
||||||
|
if err != nil {
|
||||||
|
return hash, fmt.Errorf("unable to push the new config to the remote git repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GIT) Write(
|
||||||
|
ctx context.Context,
|
||||||
|
b []byte,
|
||||||
|
) (plumbing.Hash, error) {
|
||||||
|
logger.Debugf(ctx, "Write")
|
||||||
|
defer logger.Debugf(ctx, "/Write")
|
||||||
|
|
||||||
|
worktree, err := g.Repo.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to open the git's worktree: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refs, err := g.Repo.References()
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to get git references iterator: %w", err)
|
||||||
|
}
|
||||||
|
var ref *plumbing.Reference
|
||||||
|
for {
|
||||||
|
ref, err = refs.Next()
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to get the first git reference: %w", err)
|
||||||
|
}
|
||||||
|
if ref.Name() != "HEAD" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ref == nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to find any branch references")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = worktree.Checkout(&git.CheckoutOptions{
|
||||||
|
Hash: ref.Hash(),
|
||||||
|
Force: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to checkout git HEAD: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := worktree.Filesystem.Open(
|
||||||
|
g.FilePath,
|
||||||
|
)
|
||||||
|
logger.Debugf(ctx, "file open result: %v", err)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to open file '%s' for reading: %w", g.FilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sha1SumBefore [sha1.Size]byte
|
||||||
|
|
||||||
|
if f != nil {
|
||||||
|
b, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to read file '%s': %w", g.FilePath, err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
sha1SumBefore = sha1.Sum(b)
|
||||||
|
} else {
|
||||||
|
logger.Debugf(ctx, "the file does not exist in the git repo, yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to encode the config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sha1SumAfter := sha1.Sum(b)
|
||||||
|
if bytes.Equal(sha1SumBefore[:], sha1SumAfter[:]) {
|
||||||
|
logger.Debugf(ctx, "the config didn't change: %X == %X", sha1SumBefore[:], sha1SumAfter[:])
|
||||||
|
return plumbing.Hash{}, nil
|
||||||
|
}
|
||||||
|
logger.Debugf(ctx, "the config did change: %X == %X", sha1SumBefore[:], sha1SumAfter[:])
|
||||||
|
|
||||||
|
f, err = worktree.Filesystem.OpenFile(
|
||||||
|
g.FilePath,
|
||||||
|
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to open file '%s' for writing: %w", g.FilePath, err)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(f, bytes.NewReader(b))
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to write the config into virtual git repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := g.CommitAndPush(ctx, worktree, ref)
|
||||||
|
if err != nil {
|
||||||
|
return plumbing.Hash{}, fmt.Errorf("unable to Commit&Push: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash, nil
|
||||||
|
}
|
16
pkg/repository/github.go
Normal file
16
pkg/repository/github.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/go-github/v62/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GitHub struct {
|
||||||
|
Client *github.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGitHub() *GitHub {
|
||||||
|
client := github.NewClient(nil).WithAuthToken("... your access token ...")
|
||||||
|
return &GitHub{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
}
|
@@ -3,12 +3,8 @@ package streampanel
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha1"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,329 +13,13 @@ import (
|
|||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/facebookincubator/go-belt/tool/logger"
|
"github.com/facebookincubator/go-belt/tool/logger"
|
||||||
"github.com/go-git/go-billy/v5/memfs"
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"github.com/xaionaro-go/streamctl/pkg/repository"
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
|
||||||
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
|
||||||
"github.com/go-git/go-git/v5/storage/memory"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const gitRepoPanelConfigFileName = "streampanel.yaml"
|
const gitRepoPanelConfigFileName = "streampanel.yaml"
|
||||||
|
const gitCommitterName = "streampanel"
|
||||||
type gitStorage struct {
|
const gitCommitterEmail = "streampanel@noemail.invalid"
|
||||||
RemoteURL string
|
|
||||||
PrivateKey []byte
|
|
||||||
Auth transport.AuthMethod
|
|
||||||
Repo *git.Repository
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGitStorage(
|
|
||||||
ctx context.Context,
|
|
||||||
remoteURL string,
|
|
||||||
privateKey []byte,
|
|
||||||
) (*gitStorage, error) {
|
|
||||||
stor := &gitStorage{
|
|
||||||
RemoteURL: remoteURL,
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := stor.init(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return stor, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *gitStorage) init(ctx context.Context) error {
|
|
||||||
if len(s.RemoteURL) == 0 {
|
|
||||||
return fmt.Errorf("repo URL is not provided")
|
|
||||||
}
|
|
||||||
if len(s.PrivateKey) == 0 {
|
|
||||||
return fmt.Errorf("key is not provided")
|
|
||||||
}
|
|
||||||
auth, err := gitssh.NewPublicKeys("git", s.PrivateKey, "")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create auth object for git: %w", err)
|
|
||||||
}
|
|
||||||
auth.HostKeyCallbackHelper.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.Auth = auth
|
|
||||||
|
|
||||||
repo, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
|
|
||||||
URL: s.RemoteURL,
|
|
||||||
Auth: s.Auth,
|
|
||||||
SingleBranch: true,
|
|
||||||
Mirror: true,
|
|
||||||
NoCheckout: true,
|
|
||||||
Depth: 0,
|
|
||||||
RecurseSubmodules: 0,
|
|
||||||
Progress: nil,
|
|
||||||
Tags: 0,
|
|
||||||
})
|
|
||||||
switch err {
|
|
||||||
case nil:
|
|
||||||
case plumbing.ErrReferenceNotFound, transport.ErrEmptyRemoteRepository:
|
|
||||||
repo, err = s.initGitRepo(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to initialize the git repo: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unable to clone the git repo: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Repo = repo
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrNeedsRebase struct {
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrNeedsRebase) Error() string {
|
|
||||||
return fmt.Sprintf("needs rebase: %v", err.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrNeedsRebase) Unwrap() error {
|
|
||||||
return err.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *gitStorage) initGitRepo(ctx context.Context) (*git.Repository, error) {
|
|
||||||
repo, err := git.Init(memory.NewStorage(), memfs.New())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to initialize the git local repo: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.initLocalBranch(ctx, repo); err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to make a local branch: %w", err)
|
|
||||||
}
|
|
||||||
if err := s.push(ctx, repo); err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to push the branch to the remote: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *gitStorage) initLocalBranch(
|
|
||||||
_ context.Context,
|
|
||||||
repo *git.Repository,
|
|
||||||
) error {
|
|
||||||
worktree, err := repo.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get git's worktree: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
signature := gitCommitter(now)
|
|
||||||
|
|
||||||
_, err = worktree.Commit("init repo", &git.CommitOptions{
|
|
||||||
All: false,
|
|
||||||
AllowEmptyCommits: true,
|
|
||||||
Author: signature,
|
|
||||||
Committer: signature,
|
|
||||||
Amend: false,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create the first commit in the git repo: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *gitStorage) push(
|
|
||||||
ctx context.Context,
|
|
||||||
repo interface {
|
|
||||||
PushContext(context.Context, *git.PushOptions) error
|
|
||||||
},
|
|
||||||
) error {
|
|
||||||
err := repo.PushContext(ctx, &git.PushOptions{
|
|
||||||
RemoteURL: s.RemoteURL,
|
|
||||||
Auth: s.Auth,
|
|
||||||
Progress: nil,
|
|
||||||
Prune: false,
|
|
||||||
Force: false,
|
|
||||||
InsecureSkipTLS: false,
|
|
||||||
CABundle: nil,
|
|
||||||
RequireRemoteRefs: nil,
|
|
||||||
FollowTags: false,
|
|
||||||
ForceWithLease: nil,
|
|
||||||
Options: nil,
|
|
||||||
Atomic: false,
|
|
||||||
})
|
|
||||||
logger.Debugf(ctx, "push result: %v", err)
|
|
||||||
if err == git.NoErrAlreadyUpToDate {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil && strings.Contains(err.Error(), "is at") && strings.Contains(err.Error(), "but expected") {
|
|
||||||
return ErrNeedsRebase{Err: err}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *gitStorage) readConfig(ctx context.Context) (*panelData, error) {
|
|
||||||
worktree, err := s.Repo.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to open the git's worktree: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := worktree.Filesystem.Open(gitRepoPanelConfigFileName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to open '%s': %w", gitRepoPanelConfigFileName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := io.ReadAll(f)
|
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to read the content of file '%s' from the virtual git repository: %w", gitRepoPanelConfigFileName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
panelData := newPanelData()
|
|
||||||
err = readPanelData(ctx, b, &panelData)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse the config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &panelData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *gitStorage) Pull(
|
|
||||||
ctx context.Context,
|
|
||||||
lastKnownCommit plumbing.Hash,
|
|
||||||
onUpdate func(
|
|
||||||
ctx context.Context,
|
|
||||||
newData *panelData,
|
|
||||||
),
|
|
||||||
) (_err error) {
|
|
||||||
logger.Debugf(ctx, "gitStorage.Pull")
|
|
||||||
defer func() { logger.Debugf(ctx, "/gitStorage.Pull: %v", _err) }()
|
|
||||||
|
|
||||||
err := s.Repo.FetchContext(ctx, &git.FetchOptions{
|
|
||||||
RemoteURL: s.RemoteURL,
|
|
||||||
Auth: s.Auth,
|
|
||||||
Force: true,
|
|
||||||
Prune: false,
|
|
||||||
})
|
|
||||||
if err != nil && err != git.NoErrAlreadyUpToDate {
|
|
||||||
return fmt.Errorf("unable to fetch from the remote git repo: %w", err)
|
|
||||||
}
|
|
||||||
worktree, err := s.Repo.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to open the git's worktree: %w", err)
|
|
||||||
}
|
|
||||||
if err != git.NoErrAlreadyUpToDate {
|
|
||||||
err = worktree.PullContext(ctx, &git.PullOptions{
|
|
||||||
RemoteURL: s.RemoteURL,
|
|
||||||
SingleBranch: true,
|
|
||||||
Depth: 0,
|
|
||||||
Auth: s.Auth,
|
|
||||||
RecurseSubmodules: 0,
|
|
||||||
Progress: nil,
|
|
||||||
Force: true,
|
|
||||||
InsecureSkipTLS: false,
|
|
||||||
CABundle: nil,
|
|
||||||
ProxyOptions: transport.ProxyOptions{},
|
|
||||||
})
|
|
||||||
if err != nil && err != git.NoErrAlreadyUpToDate {
|
|
||||||
return fmt.Errorf("unable to pull the updates in the git repo: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ref, err := s.Repo.Head()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get the current git ref: %w", err)
|
|
||||||
}
|
|
||||||
newCommit := ref.Hash()
|
|
||||||
|
|
||||||
err = worktree.Checkout(&git.CheckoutOptions{
|
|
||||||
Hash: newCommit,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to checkout HEAD: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newData, err := s.readConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to parse config from the git source: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastKnownCommit == newCommit {
|
|
||||||
logger.Debugf(ctx, "git is already in sync: %s == %s", lastKnownCommit, newCommit)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf(ctx, "got a new commit from git: %s -> %s", lastKnownCommit, newCommit)
|
|
||||||
newData.GitRepo.LatestSyncCommit = newCommit.String()
|
|
||||||
onUpdate(ctx, newData)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitCommitter(now time.Time) *object.Signature {
|
|
||||||
return &object.Signature{
|
|
||||||
Name: "streampanel",
|
|
||||||
Email: "streampanel@noemail.invalid",
|
|
||||||
When: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *gitStorage) CommitAndPush(
|
|
||||||
ctx context.Context,
|
|
||||||
worktree *git.Worktree,
|
|
||||||
ref *plumbing.Reference,
|
|
||||||
) (plumbing.Hash, error) {
|
|
||||||
logger.Debugf(ctx, "gitStorage.CommitAndPush")
|
|
||||||
defer logger.Debugf(ctx, "/gitStorage.CommitAndPush")
|
|
||||||
|
|
||||||
_, err := worktree.Add(gitRepoPanelConfigFileName)
|
|
||||||
if err != nil {
|
|
||||||
return plumbing.Hash{}, fmt.Errorf("unable to add file '%s' to the git's worktree: %w", gitRepoPanelConfigFileName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
signature := gitCommitter(now)
|
|
||||||
ts := now.Format(time.DateTime)
|
|
||||||
host, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
return plumbing.Hash{}, fmt.Errorf("unable to determine the host name: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hash, err := worktree.Commit(fmt.Sprintf("Update from '%s' at %s", host, ts), &git.CommitOptions{
|
|
||||||
All: true,
|
|
||||||
AllowEmptyCommits: false,
|
|
||||||
Author: signature,
|
|
||||||
Committer: signature,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return hash, fmt.Errorf("unable to commit the new config to the git repo: %w", err)
|
|
||||||
}
|
|
||||||
logger.Debugf(ctx, "new commit: %s", hash)
|
|
||||||
|
|
||||||
newRef := plumbing.NewHashReference(ref.Name(), hash)
|
|
||||||
err = s.Repo.Storer.SetReference(newRef)
|
|
||||||
if err != nil {
|
|
||||||
return hash, fmt.Errorf("unable to set git reference %#+v: %w", *newRef, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf(ctx, "had set reference: %#+v", *newRef)
|
|
||||||
|
|
||||||
err = s.push(ctx, s.Repo)
|
|
||||||
if err != nil {
|
|
||||||
return hash, fmt.Errorf("unable to push the new config to the remote git repo: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *gitStorage) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Panel) initGitIfNeeded(ctx context.Context) {
|
func (p *Panel) initGitIfNeeded(ctx context.Context) {
|
||||||
logger.Debugf(ctx, "initGitIfNeeded")
|
logger.Debugf(ctx, "initGitIfNeeded")
|
||||||
@@ -371,7 +51,14 @@ func (p *Panel) initGitIfNeeded(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf(ctx, "newGitStorage")
|
logger.Debugf(ctx, "newGitStorage")
|
||||||
gitStorage, err := newGitStorage(ctx, p.data.GitRepo.URL, []byte(p.data.GitRepo.PrivateKey))
|
gitStorage, err := repository.NewGit(
|
||||||
|
ctx,
|
||||||
|
p.data.GitRepo.URL,
|
||||||
|
[]byte(p.data.GitRepo.PrivateKey),
|
||||||
|
gitRepoPanelConfigFileName,
|
||||||
|
gitCommitterName,
|
||||||
|
gitCommitterEmail,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.displayError(fmt.Errorf("unable to sync with the remote GIT repository: %w", err))
|
p.displayError(fmt.Errorf("unable to sync with the remote GIT repository: %w", err))
|
||||||
if newUserData {
|
if newUserData {
|
||||||
@@ -541,7 +228,16 @@ func (p *Panel) gitSync(ctx context.Context) {
|
|||||||
err := p.gitStorage.Pull(
|
err := p.gitStorage.Pull(
|
||||||
ctx,
|
ctx,
|
||||||
p.getLastKnownGitCommitHash(),
|
p.getLastKnownGitCommitHash(),
|
||||||
p.onConfigUpdateViaGIT,
|
func(ctx context.Context, commitHash plumbing.Hash, b []byte) {
|
||||||
|
panelData := newPanelData()
|
||||||
|
err := readPanelData(ctx, b, &panelData)
|
||||||
|
if err != nil {
|
||||||
|
p.displayError(fmt.Errorf("unable to read panel data: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panelData.GitRepo.LatestSyncCommit = commitHash.String()
|
||||||
|
p.onConfigUpdateViaGIT(ctx, &panelData)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.displayError(fmt.Errorf("unable to sync with the remote GIT repository: %w", err))
|
p.displayError(fmt.Errorf("unable to sync with the remote GIT repository: %w", err))
|
||||||
@@ -595,103 +291,29 @@ func (p *Panel) sendConfigViaGIT(
|
|||||||
|
|
||||||
logger.Debugf(ctx, "sendConfigViaGIT: Lock() success")
|
logger.Debugf(ctx, "sendConfigViaGIT: Lock() success")
|
||||||
|
|
||||||
worktree, err := p.gitStorage.Repo.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to open the git's worktree: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
refs, err := p.gitStorage.Repo.References()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get git references iterator: %w", err)
|
|
||||||
}
|
|
||||||
var ref *plumbing.Reference
|
|
||||||
for {
|
|
||||||
ref, err = refs.Next()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get the first git reference: %w", err)
|
|
||||||
}
|
|
||||||
if ref.Name() != "HEAD" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ref == nil {
|
|
||||||
return fmt.Errorf("unable to find any branch references")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = worktree.Checkout(&git.CheckoutOptions{
|
|
||||||
Hash: ref.Hash(),
|
|
||||||
Force: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to checkout git HEAD: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := worktree.Filesystem.Open(
|
|
||||||
gitRepoPanelConfigFileName,
|
|
||||||
)
|
|
||||||
logger.Debugf(ctx, "file open result: %v", err)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("unable to open file '%s' for reading: %w", gitRepoPanelConfigFileName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sha1SumBefore [sha1.Size]byte
|
|
||||||
|
|
||||||
if f != nil {
|
|
||||||
b, err := io.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to read file '%s': %w", gitRepoPanelConfigFileName, err)
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
sha1SumBefore = sha1.Sum(b)
|
|
||||||
} else {
|
|
||||||
logger.Debugf(ctx, "the file does not exist in the git repo, yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
var newBytes bytes.Buffer
|
var newBytes bytes.Buffer
|
||||||
latestSyncCommit := p.data.GitRepo.LatestSyncCommit
|
latestSyncCommit := p.data.GitRepo.LatestSyncCommit
|
||||||
p.data.GitRepo.LatestSyncCommit = ""
|
p.data.GitRepo.LatestSyncCommit = ""
|
||||||
err = writePanelData(ctx, &newBytes, p.data)
|
err := writePanelData(ctx, &newBytes, p.data)
|
||||||
p.data.GitRepo.LatestSyncCommit = latestSyncCommit
|
p.data.GitRepo.LatestSyncCommit = latestSyncCommit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to encode the config: %w", err)
|
return fmt.Errorf("unable to serialize the panel data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sha1SumAfter := sha1.Sum(newBytes.Bytes())
|
hash, err := p.gitStorage.Write(ctx, newBytes.Bytes())
|
||||||
if bytes.Equal(sha1SumBefore[:], sha1SumAfter[:]) {
|
if errors.As(err, &repository.ErrNeedsRebase{}) {
|
||||||
logger.Debugf(ctx, "the config didn't change: %X == %X", sha1SumBefore[:], sha1SumAfter[:])
|
p.gitSync(ctx)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "the config did change: %X == %X", sha1SumBefore[:], sha1SumAfter[:])
|
|
||||||
|
|
||||||
f, err = worktree.Filesystem.OpenFile(
|
|
||||||
gitRepoPanelConfigFileName,
|
|
||||||
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
|
|
||||||
0644,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to open file '%s' for writing: %w", gitRepoPanelConfigFileName, err)
|
return fmt.Errorf("unable to write the config to the git repo: %w", err)
|
||||||
}
|
}
|
||||||
_, err = io.Copy(f, bytes.NewReader(newBytes.Bytes()))
|
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to write the config into virtual git repo: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hash, err := p.gitStorage.CommitAndPush(ctx, worktree, ref)
|
|
||||||
if !hash.IsZero() {
|
if !hash.IsZero() {
|
||||||
p.setLastKnownGitCommitHash(hash)
|
p.setLastKnownGitCommitHash(hash)
|
||||||
if err := p.saveDataToConfigFile(ctx); err != nil {
|
if err := p.saveDataToConfigFile(ctx); err != nil {
|
||||||
return fmt.Errorf("unable to store the new commit hash in the config file: %w", err)
|
return fmt.Errorf("unable to store the new commit hash in the config file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errors.As(err, &ErrNeedsRebase{}) {
|
|
||||||
p.gitSync(ctx)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to commit&push the config into virtual git repo: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/facebookincubator/go-belt/tool/logger"
|
"github.com/facebookincubator/go-belt/tool/logger"
|
||||||
"github.com/go-ng/xmath"
|
"github.com/go-ng/xmath"
|
||||||
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
|
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
|
||||||
|
"github.com/xaionaro-go/streamctl/pkg/repository"
|
||||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
|
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
|
||||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
|
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
|
||||||
@@ -70,7 +71,7 @@ type Panel struct {
|
|||||||
youtubeCheck *widget.Check
|
youtubeCheck *widget.Check
|
||||||
twitchCheck *widget.Check
|
twitchCheck *widget.Check
|
||||||
|
|
||||||
gitStorage *gitStorage
|
gitStorage *repository.GIT
|
||||||
|
|
||||||
cancelGitSyncer context.CancelFunc
|
cancelGitSyncer context.CancelFunc
|
||||||
gitSyncerMutex sync.Mutex
|
gitSyncerMutex sync.Mutex
|
||||||
@@ -1340,16 +1341,6 @@ func (p *Panel) stopStream(ctx context.Context) {
|
|||||||
p.startStopMutex.Lock()
|
p.startStopMutex.Lock()
|
||||||
defer p.startStopMutex.Unlock()
|
defer p.startStopMutex.Unlock()
|
||||||
|
|
||||||
if p.streamControllers.OBS != nil {
|
|
||||||
p.obsCheck.Enable()
|
|
||||||
}
|
|
||||||
if p.streamControllers.Twitch != nil {
|
|
||||||
p.twitchCheck.Enable()
|
|
||||||
}
|
|
||||||
if p.streamControllers.YouTube != nil {
|
|
||||||
p.youtubeCheck.Enable()
|
|
||||||
}
|
|
||||||
|
|
||||||
p.startStopButton.Disable()
|
p.startStopButton.Disable()
|
||||||
|
|
||||||
p.updateTimerHandler.Stop()
|
p.updateTimerHandler.Stop()
|
||||||
@@ -1358,7 +1349,7 @@ func (p *Panel) stopStream(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
p.updateTimerHandler = nil
|
p.updateTimerHandler = nil
|
||||||
|
|
||||||
if p.streamControllers.OBS != nil {
|
if p.obsCheck.Checked && p.streamControllers.OBS != nil {
|
||||||
p.startStopButton.SetText("Stopping OBS...")
|
p.startStopButton.SetText("Stopping OBS...")
|
||||||
err := p.streamControllers.OBS.EndStream(ctx)
|
err := p.streamControllers.OBS.EndStream(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1366,7 +1357,7 @@ func (p *Panel) stopStream(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.streamControllers.Twitch != nil {
|
if p.twitchCheck.Checked && p.streamControllers.Twitch != nil {
|
||||||
p.startStopButton.SetText("Stopping Twitch...")
|
p.startStopButton.SetText("Stopping Twitch...")
|
||||||
err := p.streamControllers.Twitch.EndStream(ctx)
|
err := p.streamControllers.Twitch.EndStream(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1374,7 +1365,7 @@ func (p *Panel) stopStream(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.streamControllers.YouTube != nil {
|
if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil {
|
||||||
p.startStopButton.SetText("Stopping YouTube...")
|
p.startStopButton.SetText("Stopping YouTube...")
|
||||||
err := p.streamControllers.YouTube.EndStream(ctx)
|
err := p.streamControllers.YouTube.EndStream(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1382,6 +1373,16 @@ func (p *Panel) stopStream(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.streamControllers.OBS != nil {
|
||||||
|
p.obsCheck.Enable()
|
||||||
|
}
|
||||||
|
if p.streamControllers.Twitch != nil {
|
||||||
|
p.twitchCheck.Enable()
|
||||||
|
}
|
||||||
|
if p.streamControllers.YouTube != nil {
|
||||||
|
p.youtubeCheck.Enable()
|
||||||
|
}
|
||||||
|
|
||||||
p.startStopButton.SetText("OnStopStream command...")
|
p.startStopButton.SetText("OnStopStream command...")
|
||||||
p.execCommand(ctx, p.data.Commands.OnStopStream)
|
p.execCommand(ctx, p.data.Commands.OnStopStream)
|
||||||
p.startStopButton.SetText("Start stream")
|
p.startStopButton.SetText("Start stream")
|
||||||
|
@@ -35,7 +35,7 @@ type gitRepoConfig struct {
|
|||||||
Enable *bool
|
Enable *bool
|
||||||
URL string `yaml:"url,omitempty"`
|
URL string `yaml:"url,omitempty"`
|
||||||
PrivateKey string `yaml:"private_key,omitempty"`
|
PrivateKey string `yaml:"private_key,omitempty"`
|
||||||
LatestSyncCommit string `yaml:"latest_sync_commit,omitempty"`
|
LatestSyncCommit string `yaml:"latest_sync_commit,omitempty"` // TODO: deprecate this field, it's just a non-needed mechanism (better to check against git history)
|
||||||
}
|
}
|
||||||
|
|
||||||
type panelData struct {
|
type panelData struct {
|
||||||
|
Reference in New Issue
Block a user