From d102633bf8f0985ec15c4fa271a5415ec7dbe5ff Mon Sep 17 00:00:00 2001 From: Mohammadreza Varasteh <58051486+The-Daishogun@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:54:41 +0330 Subject: [PATCH] Add sync command (#3) * Add sync command * Add release workflow --- .github/workflows/build-and-release.yml | 32 +++++++ cmd/commit.go | 4 +- cmd/init.go | 5 +- cmd/push.go | 12 +-- cmd/sync.go | 38 ++++++++ main.go | 2 + scripts/post-commit.sh | 4 +- structs/commit.go | 28 ++++++ structs/repo.go | 118 ++++++++++++++++++++++++ utils/git.go | 35 +++++-- utils/setup.go | 12 +-- 11 files changed, 259 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/build-and-release.yml create mode 100644 cmd/sync.go create mode 100644 structs/commit.go create mode 100644 structs/repo.go diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml new file mode 100644 index 0000000..8924ebb --- /dev/null +++ b/.github/workflows/build-and-release.yml @@ -0,0 +1,32 @@ +# .github/workflows/release.yaml + +on: + release: + types: [created] + +permissions: + contents: write + packages: write + +jobs: + releases-matrix: + name: Release Go Binary + runs-on: ubuntu-latest + strategy: + matrix: + # build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 + goos: [linux, windows, darwin] + goarch: ["386", amd64, arm64] + exclude: + - goarch: "386" + goos: darwin + - goarch: arm64 + goos: windows + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} + binary_name: "syncommit" diff --git a/cmd/commit.go b/cmd/commit.go index 0cbb2cb..180e8d2 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -5,7 +5,7 @@ import ( "log" "os" "os/exec" - "syncommit/utils" + "syncommit/structs" "github.com/spf13/cobra" ) @@ -23,7 +23,7 @@ var commitCommand = &cobra.Command{ Short: "Add a commit to the sync repo", Long: `Add a commit to the sync repo`, Run: func(cmd *cobra.Command, args []string) { - err := os.Chdir(utils.RepoPath) + err := os.Chdir(structs.RepoPath) if err != nil { log.Fatal("failed to cd into repo directory. ", err.Error()) } diff --git a/cmd/init.go b/cmd/init.go index a831572..cf2dcf0 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "syncommit/scripts" + "syncommit/structs" "syncommit/utils" "github.com/spf13/cobra" @@ -17,11 +18,11 @@ var initCommand = &cobra.Command{ Use: "init", Short: "setup the current repository to sync with github", Run: func(cmd *cobra.Command, args []string) { - privateRepoFound, err := utils.SearchDir(utils.ConfigFolderPath, utils.RepoLocation) + privateRepoFound, err := utils.SearchDir(structs.ConfigFolderPath, structs.RepoLocation) if err != nil { log.Fatal("failed to read directory") } - privateRepoUrlFound, err := utils.SearchDir(utils.ConfigFolderPath, utils.RepoFileName) + privateRepoUrlFound, err := utils.SearchDir(structs.ConfigFolderPath, structs.RepoFileName) if err != nil { log.Fatal("failed to read directory") } diff --git a/cmd/push.go b/cmd/push.go index b32ab35..dc934c0 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -3,9 +3,7 @@ package cmd import ( "fmt" "log" - "os" - "os/exec" - "syncommit/utils" + "syncommit/structs" "github.com/spf13/cobra" ) @@ -19,12 +17,8 @@ var pushCommand = &cobra.Command{ Short: "Pushes all the sync commits to github", Long: `Pushes all the sync commits to github`, Run: func(cmd *cobra.Command, args []string) { - err := os.Chdir(utils.RepoPath) - if err != nil { - log.Fatal("failed to cd into repo directory. ", err.Error()) - } - commitCmd := exec.Command("git", "push", "-fu") - err = commitCmd.Run() + syncRepo := structs.GetRepoAtPath(structs.RepoPath) + err := syncRepo.Push() if err != nil { log.Fatal("failed to push. ", err.Error()) } diff --git a/cmd/sync.go b/cmd/sync.go new file mode 100644 index 0000000..24532ce --- /dev/null +++ b/cmd/sync.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "fmt" + "log" + "syncommit/structs" + "syncommit/utils" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(syncCommand) +} + +var syncCommand = &cobra.Command{ + Use: "sync", + Short: "sync all commits to private repo", + Long: `commits and pushes all your commits in the current project to github.`, + Run: func(cmd *cobra.Command, args []string) { + repo := structs.GetRepoAtPath(".") + allCommits := repo.GetRepoCommitsForCurrentAuthor() + syncRepo := structs.GetRepoAtPath(structs.RepoPath) + syncedCommits := syncRepo.GetRepoCommitsForCurrentAuthor() + commitsToSync := utils.FilterSyncedCommits(allCommits, syncedCommits) + for _, commit := range commitsToSync { + err := commit.Commit(branchName, repo.Name) + if err != nil { + log.Fatal("failed to sync repo.\nError: ", err.Error()) + } + } + err := syncRepo.Push() + if err != nil { + log.Fatal("failed to push. ", err.Error()) + } + fmt.Println("sync commits pushed successfully.") + }, +} diff --git a/main.go b/main.go index cab085e..df40e9a 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,13 @@ package main import ( + "log" "syncommit/cmd" "syncommit/utils" ) func main() { + log.SetFlags(log.LstdFlags | log.Lshortfile) utils.RunChecks() utils.SetupConfigFolder() cmd.Execute() diff --git a/scripts/post-commit.sh b/scripts/post-commit.sh index f7e4b7c..d109ef8 100644 --- a/scripts/post-commit.sh +++ b/scripts/post-commit.sh @@ -4,10 +4,12 @@ if [[ $(git config --get remote.origin.url) == *"github"* ]]; then echo "skipping sync commit since remote is github." exit 0 fi + +commit_hash=$(git log -1 --format=%h) commit_message=$(git log -n 1 HEAD --pretty=format:%s) branch_name=$(git branch --show-current) repo_name=$(basename -s .git `git config --get remote.origin.url`) -sync_message="commit: $commit_message on branch $branch_name on repo: $repo_name" +sync_message="hash: $commit_hash $commit_message on branch $branch_name on repo: $repo_name" syncommit commit -m "$(echo $sync_message)" exit 0 \ No newline at end of file diff --git a/structs/commit.go b/structs/commit.go new file mode 100644 index 0000000..9008d3a --- /dev/null +++ b/structs/commit.go @@ -0,0 +1,28 @@ +package structs + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" + "time" +) + +type Commit struct { + Hash string + Message string + Time time.Time +} + +func (c *Commit) generateCommitMessage(repoName, branchName string) string { + return fmt.Sprintf("hash: %s %s on branch: %s on repo: %s", c.Hash, c.Message, strings.TrimSpace(repoName), strings.TrimSpace(branchName)) +} + +func (c *Commit) Commit(repoName, branchName string) error { + err := os.Chdir(RepoPath) + if err != nil { + log.Fatal("failed to cd into repo directory. ", err.Error()) + } + return exec.Command("git", "commit", "-m", fmt.Sprintf("%q", c.generateCommitMessage(repoName, branchName)), "--allow-empty", fmt.Sprintf("--date='%s'", c.Time.Format(time.RFC1123Z))).Run() +} diff --git a/structs/repo.go b/structs/repo.go new file mode 100644 index 0000000..8c1820d --- /dev/null +++ b/structs/repo.go @@ -0,0 +1,118 @@ +package structs + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" +) + +const RepoFileName = ".repo" +const RepoLocation = "repo" + +var HomePath = os.Getenv("HOME") + +var ConfigFolderPath = filepath.Join(HomePath, "/.syncommit") +var RepoPath = filepath.Join(ConfigFolderPath, RepoLocation) + +const hashPrefix = "hash: " + +type Repo struct { + Name string + Path string + BranchName string + CurrentAuthorName string + CurrentAuthorEmail string +} + +func (r *Repo) GetRepoCommitsForCurrentAuthor() (commits []Commit) { + cmd := exec.Command("git", "--no-pager", "log", fmt.Sprintf("--author=%s", r.CurrentAuthorEmail), "--pretty=format:%h$//%s$//%ad", "--date=unix", "--no-merges") + commitsBytes, err := cmd.Output() + if err != nil && err.Error() == "exit status 128" { + // User has no commits + return + } + if err != nil { + log.Fatal("Failed to get commits for current author.\nError: ", err.Error()) + } + commitsString := strings.Split(string(commitsBytes), "\n") + for _, commitString := range commitsString { + if len(commitString) < 1 { + continue + } + + var commit Commit + if r.Path != RepoPath { + commit = parseGeneralRepoCommitString(commitString) + } else { + if !strings.Contains(commitString, hashPrefix) { + continue + } + commit = parseSyncRepoCommitString(commitString) + } + commits = append(commits, commit) + } + return +} + +func parseSyncRepoCommitString(commitString string) Commit { + hashWithMessageAndTime := strings.Split(commitString, "$//") + commitTimeStr, _ := strconv.Atoi(hashWithMessageAndTime[2]) + originalCommitHash := strings.Replace(hashWithMessageAndTime[0], hashPrefix, "", 1)[0:7] + return Commit{Hash: originalCommitHash, Message: hashWithMessageAndTime[1], Time: time.Unix(int64(commitTimeStr), 0)} +} + +func parseGeneralRepoCommitString(commitString string) Commit { + hashWithMessageAndTime := strings.Split(commitString, "$//") + commitTimeStr, _ := strconv.Atoi(hashWithMessageAndTime[2]) + return Commit{Hash: hashWithMessageAndTime[0], Message: hashWithMessageAndTime[1], Time: time.Unix(int64(commitTimeStr), 0)} +} + +func (r *Repo) Push() error { + cmd := exec.Command("git", "push", "-fu") + cmd.Dir = r.Path + return cmd.Run() +} + +func GetRepoAtPath(path string) Repo { + err := os.Chdir(path) + if err != nil { + log.Fatal("failed to change directory,\nError: ", err.Error()) + } + branchNameBytes, err := exec.Command("git", "branch", "--show-current").Output() + if err != nil { + log.Fatal("failed to get repo information.\nError: ", err.Error()) + } + branchName := strings.ReplaceAll(string(branchNameBytes), "\n", "") + repoPathBytes, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + log.Fatal("failed to get repo information.\nError: ", err.Error()) + } + repoPath := strings.ReplaceAll(string(repoPathBytes), "\n", "") + repoNameBytes, err := exec.Command("basename", string(repoPath)).Output() + if err != nil { + log.Fatal("failed to get repo information.\nError: ", err.Error()) + } + repoName := strings.ReplaceAll(string(repoNameBytes), "\n", "") + authorNameBytes, err := exec.Command("git", "config", "user.name").Output() + if err != nil { + log.Fatal("failed to get repo information.\nError: ", err.Error()) + } + authorName := strings.TrimSpace(string(authorNameBytes)) + authorEmailBytes, err := exec.Command("git", "config", "user.email").Output() + if err != nil { + log.Fatal("failed to get repo information.\nError:", err.Error()) + } + authorEmail := strings.TrimSpace(string(string(authorEmailBytes))) + return Repo{ + Name: repoName, + Path: repoPath, + BranchName: branchName, + CurrentAuthorName: authorName, + CurrentAuthorEmail: authorEmail, + } +} diff --git a/utils/git.go b/utils/git.go index 820e7f1..0f33b84 100644 --- a/utils/git.go +++ b/utils/git.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "strings" + "syncommit/structs" ) func ValidateGitUrl(gitUrl string) bool { @@ -17,16 +18,16 @@ func ValidateGitUrl(gitUrl string) bool { } func ClonePrivateRepo(repoUrl string) error { - dirs, err := os.ReadDir(ConfigFolderPath) + dirs, err := os.ReadDir(structs.ConfigFolderPath) if err != nil { log.Fatal("failed to read the contents of ConfigFolderPath. ", err) } for _, dir := range dirs { - if dir.Name() == RepoLocation { + if dir.Name() == structs.RepoLocation { return nil } } - cmd := exec.Command("git", "clone", "-q", repoUrl, filepath.Join(ConfigFolderPath, RepoLocation)) + cmd := exec.Command("git", "clone", "-q", repoUrl, filepath.Join(structs.ConfigFolderPath, structs.RepoLocation)) return cmd.Run() } @@ -40,7 +41,7 @@ func GetPrivateRepo() { if !validated { log.Fatal("invalid git url. make sure it's the ssh url and the url is correct.") } - file, err := os.Create(filepath.Join(ConfigFolderPath, RepoFileName)) + file, err := os.Create(filepath.Join(structs.ConfigFolderPath, structs.RepoFileName)) if err != nil { log.Fatal("failed to create .repo file. ", err) } @@ -48,15 +49,35 @@ func GetPrivateRepo() { _, err = file.WriteString(input) if err != nil { - os.Remove(filepath.Join(ConfigFolderPath, RepoFileName)) + os.Remove(filepath.Join(structs.ConfigFolderPath, structs.RepoFileName)) log.Fatal("failed to write to .repo file. ", err) } fmt.Println("Starting to clone the repo.") err = ClonePrivateRepo(input) if err != nil { - os.Remove(filepath.Join(ConfigFolderPath, RepoFileName)) - os.Remove(filepath.Join(ConfigFolderPath, RepoLocation)) + os.Remove(filepath.Join(structs.ConfigFolderPath, structs.RepoFileName)) + os.Remove(filepath.Join(structs.ConfigFolderPath, structs.RepoLocation)) log.Fatal("failed to clone repo. make sure repo url is correct. ", err) } fmt.Println("Cloning successful.") } + +func createCommitMasterString(commits []structs.Commit) string { + var masterString = "" + for _, commit := range commits { + masterString = masterString + commit.Hash + " " + } + return masterString +} + +func FilterSyncedCommits(allCommits []structs.Commit, syncedCommits []structs.Commit) []structs.Commit { + var commitsToSync []structs.Commit + masterString := createCommitMasterString(syncedCommits) + for _, commit := range allCommits { + if strings.Contains(masterString, commit.Hash) { + continue + } + commitsToSync = append(commitsToSync, commit) + } + return commitsToSync +} diff --git a/utils/setup.go b/utils/setup.go index 05e3074..0e97c06 100644 --- a/utils/setup.go +++ b/utils/setup.go @@ -3,21 +3,13 @@ package utils import ( "log" "os" - "path/filepath" + "syncommit/structs" ) -const RepoFileName = ".repo" -const RepoLocation = "repo" - -var HomePath = os.Getenv("HOME") - -var ConfigFolderPath = filepath.Join(HomePath, "/.syncommit") -var RepoPath = filepath.Join(ConfigFolderPath, RepoLocation) - const ConfigFolderPermission = 0755 func SetupConfigFolder() { - err := os.MkdirAll(ConfigFolderPath, ConfigFolderPermission) + err := os.MkdirAll(structs.ConfigFolderPath, ConfigFolderPermission) if err != nil { log.Fatal("failed to setup", err) }