Skip to content

Commit

Permalink
implement repository reset option for status cmd (#88)
Browse files Browse the repository at this point in the history
Co-authored-by: Dawid Ciepiela <71898979-sarumaj@users.noreply.github.com>
  • Loading branch information
sarumaj and Dawid Ciepiela authored Jan 27, 2024
1 parent 2778288 commit d4630c9
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 87 deletions.
56 changes: 6 additions & 50 deletions pkg/commands/command_pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions pkg/commands/command_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}

Expand Down Expand Up @@ -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):
Expand Down
59 changes: 41 additions & 18 deletions pkg/commands/command_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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"))
Expand Down
86 changes: 79 additions & 7 deletions pkg/commands/commands_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -66,15 +69,15 @@ 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()
bar.Describe(c.CheckColors(color.BlueString, conf.GetProgressbarDescriptionForVerb(verb, repo)))
}
}

// 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 {
Expand Down Expand Up @@ -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); {

Expand All @@ -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() {
Expand All @@ -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
}
17 changes: 13 additions & 4 deletions pkg/commands/operation_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
}
Expand Down
12 changes: 9 additions & 3 deletions pkg/extras/git.aliases.json
Original file line number Diff line number Diff line change
@@ -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"
}
]

0 comments on commit d4630c9

Please sign in to comment.