Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions cmd/wasm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,9 @@ func RegisterCallbacks(api *api.Api) {
return nil, api.SetCorsProxy(args[0].String())
})
}),
"pullAll": js.FuncOf(func(this js.Value, args []js.Value) any {
"syncAll": js.FuncOf(func(this js.Value, args []js.Value) any {
return wrapPromise(func() (any, error) {
return nil, api.PullAll()
})
}),
"pushAll": js.FuncOf(func(this js.Value, args []js.Value) any {
return wrapPromise(func() (any, error) {
return nil, api.PushAll()
return nil, api.SyncAll()
})
}),
"exportZip": js.FuncOf(func(this js.Value, args []js.Value) any {
Expand Down
1 change: 1 addition & 0 deletions e2e/events_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ func TestAddEventAndGetEvent(t *testing.T) {
if err != nil {
t.Fatalf("failed to get an event by id: %v", err)
}
eventIn.UpdatedAt = eventOut.UpdatedAt

if !reflect.DeepEqual(eventIn, *eventOut) {
t.Errorf("events are not the same: \nin: %+v\n!=\nout: %+v", eventIn, *eventOut)
Expand Down
6 changes: 6 additions & 0 deletions e2e/events_repeating_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func TestAddInfinitelyRepeatingEventAndGetEvents(t *testing.T) {
if err != nil {
t.Fatalf("failed to get an event by id: %v", err)
}
eventIn.UpdatedAt = eventOut.UpdatedAt

if !reflect.DeepEqual(eventIn, *eventOut) {
t.Errorf("events are not the same: \nin: %+v\n!=\nout: %+v", eventIn, *eventOut)
}
Expand Down Expand Up @@ -96,6 +98,8 @@ func TestAddCountRepeatingEventAndGetEvents(t *testing.T) {
if err != nil {
t.Fatalf("failed to get an event by id: %v", err)
}
eventIn.UpdatedAt = eventOut.UpdatedAt

if !reflect.DeepEqual(eventIn, *eventOut) {
t.Errorf("events are not the same: \nin: %+v\n!=\nout: %+v", eventIn, *eventOut)
}
Expand Down Expand Up @@ -144,6 +148,8 @@ func TestAddRepeatingEventsAndRemoveRepeatingEvent(t *testing.T) {
if err != nil {
t.Fatalf("failed to get an event by id: %v", err)
}
eventIn.UpdatedAt = eventOut.UpdatedAt

if !reflect.DeepEqual(eventIn, *eventOut) {
t.Errorf("events are not the same: \nin: %+v\n!=\nout: %+v", eventIn, *eventOut)
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ func (a *Api) RenameCalendar(oldName, newName string) error {
func (a *Api) LoadCalendars() error { return a.inner.LoadCalendars() }

func (a *Api) SetCorsProxy(proxyUrl string) error { return a.inner.SetCorsProxy(proxyUrl) }
func (a *Api) PullAll() error { return a.inner.PullAll() }
func (a *Api) PushAll() error { return a.inner.PushAll() }
func (a *Api) SyncAll() error { return a.inner.SyncAll() }
func (a *Api) ExportZip(calendar string) ([]byte, error) { return a.inner.ExportZip(calendar) }

// ------------------------------ Wrapper methods encoding and decoding JSONs ------------------------------
Expand Down
2 changes: 2 additions & 0 deletions pkg/core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const (
EventsDirName string = "events"

GitAuthorName string = "git-calendar"
GitRemoteName string = "origin"
GitBranchName string = "main"
)

// ------- Repeating frequency -------
Expand Down
110 changes: 44 additions & 66 deletions pkg/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/git-calendar/core/pkg/export"
"github.com/git-calendar/core/pkg/filesystem"
"github.com/git-calendar/core/pkg/gitmerge"
"github.com/go-git/go-billy/v5"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/cache"
Expand Down Expand Up @@ -50,87 +51,64 @@ func (c *Core) SetCorsProxy(proxyUrl string) error {
return err
}

// Update all remotes for all repositories.
func (c *Core) PushAll() error {
var errs error
// SyncAll tries to synchronize all calendars with its remotes.
func (c *Core) SyncAll() error {
var resultErr error

for _, cal := range c.calendars {
remotes, err := cal.repository.Remotes()
if err != nil {
errs = errors.Join(err)
if cal == nil || cal.repository == nil {
continue // important to check here; syncCalendar and other do assume this
}

for _, remote := range remotes {
err = remote.Push(&gogit.PushOptions{})
if err == gogit.NoErrAlreadyUpToDate {
continue // this is ok
}
if err != nil {
errs = errors.Join(err)
}
if err := c.syncCalendar(cal); err != nil {
resultErr = errors.Join(resultErr, fmt.Errorf("%q: sync failed: %w", cal.Name, err))
}
}
return errs
}

func (c *Core) PullAll() error {
var resultErr error
if err := c.LoadCalendars(); err != nil { // reload events from disk
resultErr = errors.Join(resultErr, err)
}

for _, cal := range c.calendars {
if cal == nil || cal.repository == nil {
continue
return resultErr
}

// syncCalendar assumes the worktree is clean and all local calendar changes have already been committed.
func (c *Core) syncCalendar(cal *calendar) error {
if err := fetchCalendar(cal, c.proxyUrl); err != nil {
if errors.Is(err, gogit.ErrRemoteNotFound) {
return nil // this is ok
}
return err
}

fmt.Println("pulling", cal.Name)
localCommit, remoteCommit, err := gitmerge.GetCommits(cal.repository, GitBranchName, GitRemoteName)
if err != nil {
return err
}

wt, err := cal.repository.Worktree()
if err != nil {
resultErr = errors.Join(resultErr, fmt.Errorf("%q: get worktree: %w", cal.Name, err))
continue
}
if wt == nil {
continue
}
switch {
case localCommit.Hash == remoteCommit.Hash:
return nil // already in sync

remote, err := cal.repository.Remote("origin")
if err != nil {
if errors.Is(err, gogit.ErrRemoteNotFound) {
continue // this is ok
}
resultErr = errors.Join(resultErr, fmt.Errorf("%q: get remote: %w", cal.Name, err))
continue
}
case isAncestor(localCommit, remoteCommit):
// remote is ahead
return fastForwardCalendar(cal, remoteCommit.Hash)

cfg := remote.Config()
if cfg == nil {
resultErr = errors.Join(resultErr, fmt.Errorf("%q: remote has no config", cal.Name))
continue
}
if len(cfg.URLs) != 1 || cfg.URLs[0] == "" {
resultErr = errors.Join(resultErr, fmt.Errorf("%q: remote must have exactly one non-empty URL", cal.Name))
continue
}
repoURL, err := url.Parse(cfg.URLs[0])
if err != nil {
resultErr = errors.Join(resultErr, fmt.Errorf("%q: parse remote URL: %w", cal.Name, err))
continue
}
case isAncestor(remoteCommit, localCommit):
// local is ahead
return pushCalendar(cal, c.proxyUrl)

finalURL, auth := prepareRepoUrl(repoURL, c.proxyUrl)
err = wt.Pull(&gogit.PullOptions{
RemoteName: "origin",
RemoteURL: finalURL.String(),
Auth: auth,
})
if errors.Is(err, gogit.NoErrAlreadyUpToDate) {
continue // this is ok
}
if err != nil {
resultErr = errors.Join(resultErr, fmt.Errorf("%q: pull from origin failed: %w", cal.Name, err))
continue
default:
// cant simply push or pull (history diverged) -> try merge

fmt.Printf("Diverged history detected on %q, trying to merge...\n", cal.Name)
if err := mergeOriginMain(cal.repository); err != nil {
return fmt.Errorf("failed to merge: %w", err)
}
}
fmt.Printf("Custom merge successfull for %q\n", cal.Name)

return resultErr
return pushCalendar(cal, c.proxyUrl)
}
}

func (c *Core) ExportZip(calendar string) ([]byte, error) {
Expand Down
12 changes: 7 additions & 5 deletions pkg/core/core_calendars.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (c *Core) ListCalendars() ([]CalendarInfo, error) {
for _, cal := range calendars {
var remoteUrl string
if cal.repository != nil {
remote, err := cal.repository.Remote("origin")
remote, err := cal.repository.Remote(GitRemoteName)
if err == nil {
cfg := remote.Config()
if cfg == nil {
Expand Down Expand Up @@ -208,7 +208,7 @@ func (c *Core) CloneCalendar(repoUrl *url.URL, password string) error {
finalUrl, auth := prepareRepoUrl(repoUrl, c.proxyUrl)
// clone now
newRepo, err := gogit.Clone(storage, repoFS, &gogit.CloneOptions{
RemoteName: "origin",
RemoteName: GitRemoteName,
URL: finalUrl.String(),
Auth: auth,
})
Expand Down Expand Up @@ -300,6 +300,8 @@ func (c *Core) RenameCalendar(oldName, newName string) error {
delete(c.calendars, oldName)
c.calendars[newName] = calendar

// TODO: handle "calendar" field in its events

return nil
}

Expand All @@ -314,7 +316,7 @@ func (c *Core) UpdateRemote(calendar string, remoteURL *url.URL) error {
}

if remoteURL == nil {
if err := cal.repository.DeleteRemote("origin"); err != nil {
if err := cal.repository.DeleteRemote(GitRemoteName); err != nil {
if errors.Is(err, gogit.ErrRemoteNotFound) {
return nil
}
Expand All @@ -328,8 +330,8 @@ func (c *Core) UpdateRemote(calendar string, remoteURL *url.URL) error {
}

cfg, _ := cal.repository.Config()
cfg.Remotes["origin"] = &config.RemoteConfig{
Name: "origin",
cfg.Remotes[GitRemoteName] = &config.RemoteConfig{
Name: GitRemoteName,
URLs: []string{remoteURL.String()},
}
if err := cal.repository.SetConfig(cfg); err != nil {
Expand Down
2 changes: 2 additions & 0 deletions pkg/core/core_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ func (c *Core) removeCurrentChild(event *Event) error {

// Serializes event to JSON, saves to file, stages and commits with given message.
func (c *Core) saveAndCommitEvent(event *Event, commitMsg string) error {
event.UpdatedAt = time.Now() // force new time

// -------- write to disk --------
cal, ok := c.calendars[event.Calendar]
if !ok {
Expand Down
1 change: 1 addition & 0 deletions pkg/core/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Event struct {
Tag string `json:"tag,omitzero"` // User-defined category or label.
ParentId uuid.UUID `json:"parent_id,omitzero"` // Specific for child events. It is uuid.Nil if the event is basic or parent.
Repeat *Repetition `json:"repeat,omitzero"`
UpdatedAt time.Time `json:"updated_at,omitzero"` // Used for git conflict resolution; latest wins.
}

// Repetition defines the recurrence rules for a Parent event.
Expand Down
116 changes: 116 additions & 0 deletions pkg/core/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package core

import (
"encoding/json"
"errors"
"fmt"
"net/url"
"path"
"time"

"github.com/git-calendar/core/pkg/gitmerge"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)

func pushCalendar(cal *calendar, proxyUrl *url.URL) error {
fmt.Println("pushing", cal.Name)

repoUrl, err := repoUrlFromCalendar(cal)
if err != nil {
return err
}

finalUrl, auth := prepareRepoUrl(repoUrl, proxyUrl)
return ignoreUpToDate(cal.repository.Push(&gogit.PushOptions{
RemoteName: GitRemoteName,
RemoteURL: finalUrl.String(),
Auth: auth,
}))
}

func fetchCalendar(cal *calendar, proxyUrl *url.URL) error {
fmt.Println("fetching", cal.Name)

repoUrl, err := repoUrlFromCalendar(cal)
if err != nil {
return err
}

finalUrl, auth := prepareRepoUrl(repoUrl, proxyUrl)
return ignoreUpToDate(cal.repository.Fetch(&gogit.FetchOptions{
RemoteName: GitRemoteName,
RemoteURL: finalUrl.String(),
Auth: auth,
}))
}

func fastForwardCalendar(cal *calendar, hash plumbing.Hash) error {
fmt.Println("fast-forward", cal.Name)

wt, err := cal.repository.Worktree()
if err != nil {
return err
}

if err := wt.Checkout(&gogit.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(GitBranchName),
}); err != nil {
return fmt.Errorf("failed to checkout %s: %w", GitBranchName, err)
}

if err := wt.Reset(&gogit.ResetOptions{
Commit: hash,
Mode: gogit.HardReset,
}); err != nil {
return fmt.Errorf("failed to fast-forward %s to %s: %w", GitBranchName, hash, err)
}

return nil
}

// ignoreUpToDate swallows the benign "already up to date" error from push/fetch.
func ignoreUpToDate(err error) error {
if errors.Is(err, gogit.NoErrAlreadyUpToDate) {
return nil
}
return err
}

// isAncestor reports whether a is an ancestor of (or equal to) b.
func isAncestor(a, b *object.Commit) bool {
if a.Hash == b.Hash {
return true
}
ok, err := a.IsAncestor(b)
if err != nil {
fmt.Printf("WARN: isAncestor %s -> %s: %v\n", a.Hash, b.Hash, err)
return false
}
return ok
}

// mergeOriginMain is an "adapter" for gitmerge.MergeRemoteIntoBranch.
func mergeOriginMain(repo *gogit.Repository) error {
return gitmerge.MergeRemoteIntoBranch(repo, gitmerge.Options{
BranchName: GitBranchName,
RemoteName: GitRemoteName,

AuthorName: GitAuthorName,

IncludePath: func(gitPath string) bool {
return path.Dir(gitPath) == EventsDirName &&
path.Ext(gitPath) == ".json"
},

UpdatedAt: func(gitPath string, data []byte) (time.Time, error) {
var ev Event
if err := json.Unmarshal(data, &ev); err != nil {
return time.Time{}, fmt.Errorf("%s: failed to parse event JSON: %w", gitPath, err)
}

return ev.UpdatedAt, nil
},
})
}
Loading