From d4630c90881c9a00162576c983119f2b3e140c17 Mon Sep 17 00:00:00 2001 From: Dawid Ciepiela <71898979+sarumaj@users.noreply.github.com> Date: Sat, 27 Jan 2024 13:49:49 +0100 Subject: [PATCH] implement repository reset option for status cmd (#88) Co-authored-by: Dawid Ciepiela <71898979-sarumaj@users.noreply.github.com> --- pkg/commands/command_pull.go | 56 +++------------------- pkg/commands/command_push.go | 6 +-- pkg/commands/command_status.go | 59 ++++++++++++++++------- pkg/commands/commands_util.go | 86 +++++++++++++++++++++++++++++++--- pkg/commands/operation_loop.go | 17 +++++-- pkg/extras/git.aliases.json | 12 +++-- 6 files changed, 149 insertions(+), 87 deletions(-) diff --git a/pkg/commands/command_pull.go b/pkg/commands/command_pull.go index 1e31654..d575680 100644 --- a/pkg/commands/command_pull.go +++ b/pkg/commands/command_pull.go @@ -18,7 +18,7 @@ var pullCmd = &cobra.Command{ Short: "Pull all repositories", Example: "gh pr pull", Run: func(*cobra.Command, []string) { - operationLoop(pullOperation, "Pull") + operationLoop(pullOperation, "Pull", nil) }, } @@ -70,10 +70,6 @@ func pullExistingRepository(repo configfile.Repository, status *operationStatus) RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, }); { - case errors.Is(err, git.ErrNonFastForwardUpdate): - status.appendRow(repo.Directory, fmt.Errorf("non-fast-forward update")) - return nil, nil, fmt.Errorf("repository %s: %w", repo.Directory, err) - case errors.Is(err, git.NoErrAlreadyUpToDate): // ignore case err != nil: @@ -119,6 +115,7 @@ func pullOperation(_ pool.WorkUnit, args operationContext) { logger.Debug("Overwriting repo config") host := util.GetHostnameFromPath(repo.URL) + // update remote URL to use current personal access token if err := updateRepoConfig(conf, host, repository); err != nil { logger.Debugf("Failed to update repo config: %v", err) status.appendRow(repo.Directory, err) @@ -227,53 +224,12 @@ func pullSubmodule(submodule *git.Submodule) error { } } - if err := worktree.Pull(&git.PullOptions{}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { - - // Ignore NoErrAlreadyUpToDate - return fmt.Errorf("submodule %s: %w", status.Path, err) - } - - return nil -} - -// Set username and email in the repository config. -func updateRepoConfig(conf *configfile.Configuration, host string, repository *git.Repository) error { - repoConf, err := repository.Config() - if err != nil { - return err - } - - profilesMap := conf.Profiles.ToMap() - profile, ok := profilesMap[host] - if !ok { - return fmt.Errorf("no profile for host: %q", host) - } - - // set user - repoConf.User.Name = profile.Fullname - repoConf.User.Email = profile.Email - - // update remote "origin" urls to use current authentication context - if cfg, ok := repoConf.Remotes["origin"]; ok { - for i := range cfg.URLs { - conf.AuthenticateURL(&cfg.URLs[i]) - } - - repoConf.Remotes["origin"] = cfg - } - - // update submodules' urls to use current authentication context - for name, cfg := range repoConf.Submodules { - conf.AuthenticateURL(&cfg.URL) - repoConf.Submodules[name] = cfg - } + switch err := worktree.Pull(&git.PullOptions{}); { - if err := repoConf.Validate(); err != nil { - return err - } + case err == nil, errors.Is(err, git.NoErrAlreadyUpToDate): // ignore - if err := repository.Storer.SetConfig(repoConf); err != nil { - return err + default: + return fmt.Errorf("submodule %s: %w", status.Path, err) } return nil diff --git a/pkg/commands/command_push.go b/pkg/commands/command_push.go index ccf6d3d..e387ae2 100644 --- a/pkg/commands/command_push.go +++ b/pkg/commands/command_push.go @@ -18,7 +18,7 @@ var pushCmd = &cobra.Command{ Short: "Push all repositories", Example: "gh pr push", Run: func(*cobra.Command, []string) { - operationLoop(pushOperation, "Push") + operationLoop(pushOperation, "Push", nil) }, } @@ -54,10 +54,6 @@ func pushRepository(repo configfile.Repository, status *operationStatus) error { switch err := repository.Push(&git.PushOptions{}); { - case errors.Is(err, git.ErrNonFastForwardUpdate): - status.appendRow(repo.Directory, fmt.Errorf("non-fast-forward update")) - return fmt.Errorf("repository %s: %w", repo.Directory, err) - case errors.Is(err, transport.ErrAuthenticationRequired), errors.Is(err, transport.ErrAuthorizationFailed): diff --git a/pkg/commands/command_status.go b/pkg/commands/command_status.go index 19ecf0e..1e95fd0 100644 --- a/pkg/commands/command_status.go +++ b/pkg/commands/command_status.go @@ -10,32 +10,46 @@ import ( pool "gopkg.in/go-playground/pool.v3" ) +// statusFlags represents flags for status command +var statusFlags struct { + reset bool +} + // statusCmd represents the status command -var statusCmd = &cobra.Command{ - Use: "status", - Short: "Show status for all repositories", - Long: "Show status for all repositories.\n\n" + - "Additionally, untracked directories will be listed.", - Example: "gh gr status", - Run: func(*cobra.Command, []string) { - operationLoop(statusOperation, "Check") - - conf := configfile.Load() - status := newOperationStatus() - - for _, f := range conf.ListUntracked() { - status.appendRow(f, fmt.Errorf("untracked")) - } +var statusCmd = func() *cobra.Command { + statusCmd := &cobra.Command{ + Use: "status", + Short: "Show status for all repositories", + Long: "Show status for all repositories.\n\n" + + "Additionally, untracked directories will be listed.", + Example: "gh gr status", + Run: func(*cobra.Command, []string) { + operationLoop(statusOperation, "Check", operationContextMap{"reset": statusFlags.reset}) + + conf := configfile.Load() + status := newOperationStatus() + + for _, f := range conf.ListUntracked() { + status.appendRow(f, fmt.Errorf("untracked")) + } - status.Sort().Print() - }, -} + status.Sort().Print() + }, + } + + flags := statusCmd.Flags() + flags.BoolVar(&statusFlags.reset, "reset-all", false, "Perform hard reset against remote for each dirty local repository "+ + "(it will discard all not staged and not committed changes)") + + return statusCmd +}() // Check status of local repository. func statusOperation(_ pool.WorkUnit, args operationContext) { conf := unwrapOperationContext[*configfile.Configuration](args, "conf") repo := unwrapOperationContext[configfile.Repository](args, "repo") status := unwrapOperationContext[*operationStatus](args, "status") + reset := unwrapOperationContext[bool](args, "reset") logger := loggerEntry.WithField("command", "status").WithField("repository", repo.Directory) @@ -88,6 +102,15 @@ func statusOperation(_ pool.WorkUnit, args operationContext) { if repoStatus.IsClean() { ret = append(ret, "clean") + } else if reset { + if err := resetRepository(workTree, head); err != nil { + logger.Debugf("Failed to reset repository worktree: %v", err) + status.appendRow(repo.Directory, err) + return + + } + ret = append(ret, "reset") + } else { logger.Debug("Repository is dirty") ret = append(ret, fmt.Errorf("dirty")) diff --git a/pkg/commands/commands_util.go b/pkg/commands/commands_util.go index af6c745..1d589ec 100644 --- a/pkg/commands/commands_util.go +++ b/pkg/commands/commands_util.go @@ -11,6 +11,7 @@ import ( color "github.com/fatih/color" git "github.com/go-git/go-git/v5" gitconfig "github.com/go-git/go-git/v5/config" + plumbing "github.com/go-git/go-git/v5/plumbing" configfile "github.com/sarumaj/gh-gr/v2/pkg/configfile" extras "github.com/sarumaj/gh-gr/v2/pkg/extras" restclient "github.com/sarumaj/gh-gr/v2/pkg/restclient" @@ -19,11 +20,12 @@ import ( logrus "github.com/sirupsen/logrus" ) -// Write git alias commands into local repository config file. +// addGitAliases adds git aliases to .gitconfig. func addGitAliases() error { var ga []struct { - Alias string `json:"alias"` - Command string `json:"command"` + Alias string `json:"alias"` + Description string `json:"description"` + Command string `json:"command"` } if err := json.Unmarshal(extras.GitAliasesJSON, &ga); err != nil { return err @@ -48,6 +50,7 @@ func addGitAliases() error { section := cfg.Raw.Section("alias") for _, alias := range ga { section.SetOption(alias.Alias, alias.Command) + section.SetOption(alias.Alias+".description", alias.Description) } if err := cfg.Validate(); err != nil { @@ -66,7 +69,7 @@ func addGitAliases() error { return nil } -// Change progressbar description for given repository. +// changeProgressbarText changes progressbar text. func changeProgressbarText(bar *util.Progressbar, conf *configfile.Configuration, verb string, repo configfile.Repository) { if bar != nil && conf != nil { c := util.Console() @@ -74,7 +77,7 @@ func changeProgressbarText(bar *util.Progressbar, conf *configfile.Configuration } } -// Initialize new configuration or update existing one. +// initializeOrUpdateConfig initializes or updates app configuration. func initializeOrUpdateConfig(conf *configfile.Configuration, update bool) { var logger *logrus.Entry if update { @@ -156,7 +159,7 @@ func initializeOrUpdateConfig(conf *configfile.Configuration, update bool) { conf.Save() } -// Open local repository. +// openRepository opens repository at given path. func openRepository(repo configfile.Repository, status *operationStatus) (*git.Repository, error) { switch repository, err := git.PlainOpen(repo.Directory); { @@ -174,7 +177,28 @@ func openRepository(repo configfile.Repository, status *operationStatus) (*git.R } } -// Update configFlags from loaded configuration. +// resetRepository resets repository to given head. +func resetRepository(workTree *git.Worktree, head *plumbing.Reference) error { + if err := workTree.Reset(&git.ResetOptions{ + Mode: git.HardReset, + Commit: head.Hash(), + }); err != nil { + return err + } + + repoStatus, err := workTree.Status() + if err != nil { + return err + } + + if !repoStatus.IsClean() { + return git.ErrWorktreeNotClean + } + + return nil +} + +// updateConfigFlags updates global configuration flags. func updateConfigFlags() { var conf *configfile.Configuration if configfile.ConfigurationExists() { @@ -185,3 +209,51 @@ func updateConfigFlags() { configFlags = conf } } + +// updateRepoConfig updates repository config. +// If host is specified, it will update user name and email. +// It will update remote "origin" and submodules' urls to use current personal access token. +func updateRepoConfig(conf *configfile.Configuration, host string, repository *git.Repository) error { + repoConf, err := repository.Config() + if err != nil { + return err + } + + // set user if host is specified + if host != "" { + profilesMap := conf.Profiles.ToMap() + profile, ok := profilesMap[host] + if !ok { + return fmt.Errorf("no profile for host: %q", host) + } + + // set user + repoConf.User.Name = profile.Fullname + repoConf.User.Email = profile.Email + } + + // update remote "origin" urls to use current authentication context + if cfg, ok := repoConf.Remotes["origin"]; ok { + for i := range cfg.URLs { + conf.AuthenticateURL(&cfg.URLs[i]) + } + + repoConf.Remotes["origin"] = cfg + } + + // update submodules' urls to use current authentication context + for name, cfg := range repoConf.Submodules { + conf.AuthenticateURL(&cfg.URL) + repoConf.Submodules[name] = cfg + } + + if err := repoConf.Validate(); err != nil { + return err + } + + if err := repository.Storer.SetConfig(repoConf); err != nil { + return err + } + + return nil +} diff --git a/pkg/commands/operation_loop.go b/pkg/commands/operation_loop.go index 0d8c209..6ac1c79 100644 --- a/pkg/commands/operation_loop.go +++ b/pkg/commands/operation_loop.go @@ -10,7 +10,7 @@ import ( ) // Wrapper for repository operations (e.g. pull, push, status). -func operationLoop(fn func(pool.WorkUnit, operationContext), verbInfinitive string) { +func operationLoop(fn func(pool.WorkUnit, operationContext), verbInfinitive string, args operationContextMap) { logger := loggerEntry bar := util.NewProgressbar(100) @@ -39,11 +39,20 @@ func operationLoop(fn func(pool.WorkUnit, operationContext), verbInfinitive stri return nil, wu.Error() } - fn(wu, newOperationContext(operationContextMap{ - "conf": conf, + ctx := operationContextMap{ "repo": repo, "status": status, - })) + "conf": conf, + } + + for k, v := range args { + if _, ok := ctx[k]; ok { + continue + } + ctx[k] = v + } + + fn(wu, newOperationContext(ctx)) return repo, nil } diff --git a/pkg/extras/git.aliases.json b/pkg/extras/git.aliases.json index 41bfdc1..b8da531 100644 --- a/pkg/extras/git.aliases.json +++ b/pkg/extras/git.aliases.json @@ -1,26 +1,32 @@ [ { "alias": "wip", + "description": "Stage all changes made to tracked files and commit with a WIP message", "command": "!git add -u && git commit -m \"WIP\"" }, { "alias": "undo", + "description": "Undo last commit by un-staging the changes but keeping them locally", "command": "reset HEAD~1 --mixed" }, { "alias": "fup", - "command": "!git pull upstream main && git push" + "description": "Fast pull upstream from remote head branch to the current branch", + "command": "!git pull upstream $(git rev-parse --abbrev-ref HEAD) && git push" }, { "alias": "bcl", - "command": "!git checkout main && git branch --merged main | grep -v \"main$\" | xargs -n 1 git branch -d" + "description": "Delete all local branches that have been merged into the current branch", + "command": "\"!f() { branch=$(git rev-parse --abbrev-ref HEAD); git checkout $branch && git branch --merged $branch | grep -v \"$branch$\" | xargs -n 1 git branch -d; }; f\"" }, { "alias": "nub", - "command": "\"!f() { git checkout main && git fup && git checkout -b ${1-main}; }; f\"" + "description": "Create a new branch from the current branch and pull upstream from remote head branch", + "command": "\"!f() { branch=$(git rev-parse --abbrev-ref HEAD); git checkout $branch && git fup && git checkout -b ${1-$branch}; }; f\"" }, { "alias": "cap", + "description": "Append all changes to the last commit and force push to the current branch", "command": "!git add . && git commit --amend --no-edit && git push -f" } ]