Skip to content

Commit 21ac6e3

Browse files
authored
Fix stdout/stderr handling (#27)
1 parent 440dcf1 commit 21ac6e3

File tree

4 files changed

+92
-80
lines changed

4 files changed

+92
-80
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
# This script writes some text to stdout and stderr and then exits with an error. This is used to test that git-xargs
3+
# always logs the stdout and stderr from scripts, even if those scripts exit with an error.
4+
5+
echo 'Hello, from STDOUT'
6+
>&2 echo 'Hello, from STDERR'
7+
exit 1

cmd/process.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ func processRepo(config *GitXargsConfig, repo *github.Repository) error {
7272
}
7373

7474
//Run the specified command
75-
commandErr := executeCommand(config, repositoryDir, repo, worktree)
75+
commandErr := executeCommand(config, repositoryDir, repo)
7676
if commandErr != nil {
7777
return commandErr
7878
}
7979

8080
// Commit any untracked files, modified or deleted files that resulted from script execution
81-
commitErr := commitLocalChanges(config, worktree, repo, localRepository)
81+
commitErr := commitLocalChanges(config, repositoryDir, worktree, repo, localRepository)
8282
if commitErr != nil {
8383
return commitErr
8484
}

cmd/repo-operations.go

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,14 @@ func getLocalRepoHeadRef(config *GitXargsConfig, localRepository *git.Repository
8282
return ref, nil
8383
}
8484

85-
// executeCommand runs the user-supplied command and runs it against the given repository, adding any git changes that may occur
86-
func executeCommand(config *GitXargsConfig, repositoryDir string, repo *github.Repository, worktree *git.Worktree) error {
87-
88-
logger := logging.GetLogger("git-xargs")
85+
// executeCommand runs the user-supplied command against the given repository
86+
func executeCommand(config *GitXargsConfig, repositoryDir string, repo *github.Repository) error {
87+
return executeCommandWithLogger(config, repositoryDir, repo, logging.GetLogger("git-xargs"))
88+
}
8989

90+
// executeCommandWithLogger runs the user-supplied command against the given repository, and sends the log output
91+
// to the given logger
92+
func executeCommandWithLogger(config *GitXargsConfig, repositoryDir string, repo *github.Repository, logger *logrus.Logger) error {
9093
if len(config.Args) < 1 {
9194
return errors.WithStackTrace(NoCommandSuppliedErr{})
9295
}
@@ -104,6 +107,10 @@ func executeCommand(config *GitXargsConfig, repositoryDir string, repo *github.R
104107

105108
stdoutStdErr, err := cmd.CombinedOutput()
106109

110+
logger.WithFields(logrus.Fields{
111+
"CombinedOutput": string(stdoutStdErr),
112+
}).Debug("Received output of command run")
113+
107114
if err != nil {
108115
logger.WithFields(logrus.Fields{
109116
"Error": err,
@@ -113,58 +120,6 @@ func executeCommand(config *GitXargsConfig, repositoryDir string, repo *github.R
113120
return errors.WithStackTrace(err)
114121
}
115122

116-
logger.WithFields(logrus.Fields{
117-
"CombinedOutput": string(stdoutStdErr),
118-
}).Debug("Received output of command run")
119-
120-
status, statusErr := worktree.Status()
121-
122-
if statusErr != nil {
123-
logger.WithFields(logrus.Fields{
124-
"Error": statusErr,
125-
"Repo": repo.GetName(),
126-
"Dir": repositoryDir,
127-
}).Debug("Error looking up worktree status")
128-
129-
// Track the status check failure
130-
config.Stats.TrackSingle(WorktreeStatusCheckFailedCommand, repo)
131-
return errors.WithStackTrace(statusErr)
132-
}
133-
134-
// If the supplied command resulted in any changes, we need to stage, add and commit them
135-
if !status.IsClean() {
136-
logger.WithFields(logrus.Fields{
137-
"Repo": repo.GetName(),
138-
}).Debug("Local repository worktree no longer clean, will stage and add new files and commit changes")
139-
140-
// Track the fact that worktree changes were made following execution
141-
config.Stats.TrackSingle(WorktreeStatusDirty, repo)
142-
143-
for filepath := range status {
144-
if status.IsUntracked(filepath) {
145-
fmt.Printf("Found untracked file %s. Adding to stage", filepath)
146-
_, addErr := worktree.Add(filepath)
147-
if addErr != nil {
148-
logger.WithFields(logrus.Fields{
149-
"Error": addErr,
150-
"Filepath": filepath,
151-
}).Debug("Error adding file to git stage")
152-
// Track the file staging failure
153-
config.Stats.TrackSingle(WorktreeAddFileFailed, repo)
154-
return errors.WithStackTrace(addErr)
155-
}
156-
}
157-
}
158-
159-
} else {
160-
logger.WithFields(logrus.Fields{
161-
"Repo": repo.GetName(),
162-
}).Debug("Local repository status is clean - nothing to stage or commit")
163-
164-
// Track the fact that repo had no file changes post command execution
165-
config.Stats.TrackSingle(WorktreeStatusClean, repo)
166-
}
167-
168123
return nil
169124
}
170125

@@ -251,12 +206,61 @@ func checkoutLocalBranch(config *GitXargsConfig, ref *plumbing.Reference, worktr
251206
return branchName, nil
252207
}
253208

254-
// commitLocalChanges will create a commit using the supplied or default commit message and will add any untracked, deleted
255-
// or modified files that resulted from script execution
256-
func commitLocalChanges(config *GitXargsConfig, worktree *git.Worktree, remoteRepository *github.Repository, localRepository *git.Repository) error {
257-
209+
// commitLocalChanges will check for any changes in worktree as a result of script execution, and if any are present,
210+
// add any untracked, deleted or modified files and create a commit using the supplied or default commit message.
211+
func commitLocalChanges(config *GitXargsConfig, repositoryDir string, worktree *git.Worktree, remoteRepository *github.Repository, localRepository *git.Repository) error {
258212
logger := logging.GetLogger("git-xargs")
259213

214+
status, statusErr := worktree.Status()
215+
216+
if statusErr != nil {
217+
logger.WithFields(logrus.Fields{
218+
"Error": statusErr,
219+
"Repo": remoteRepository.GetName(),
220+
"Dir": repositoryDir,
221+
}).Debug("Error looking up worktree status")
222+
223+
// Track the status check failure
224+
config.Stats.TrackSingle(WorktreeStatusCheckFailedCommand, remoteRepository)
225+
return errors.WithStackTrace(statusErr)
226+
}
227+
228+
// If there are no changes, we log it, track it, and return
229+
if status.IsClean() {
230+
logger.WithFields(logrus.Fields{
231+
"Repo": remoteRepository.GetName(),
232+
}).Debug("Local repository status is clean - nothing to stage or commit")
233+
234+
// Track the fact that repo had no file changes post command execution
235+
config.Stats.TrackSingle(WorktreeStatusClean, remoteRepository)
236+
237+
return nil
238+
}
239+
240+
// If there are changes, we need to stage, add and commit them
241+
logger.WithFields(logrus.Fields{
242+
"Repo": remoteRepository.GetName(),
243+
}).Debug("Local repository worktree no longer clean, will stage and add new files and commit changes")
244+
245+
// Track the fact that worktree changes were made following execution
246+
config.Stats.TrackSingle(WorktreeStatusDirty, remoteRepository)
247+
248+
for filepath := range status {
249+
if status.IsUntracked(filepath) {
250+
fmt.Printf("Found untracked file %s. Adding to stage", filepath)
251+
_, addErr := worktree.Add(filepath)
252+
if addErr != nil {
253+
logger.WithFields(logrus.Fields{
254+
"Error": addErr,
255+
"Filepath": filepath,
256+
}).Debug("Error adding file to git stage")
257+
// Track the file staging failure
258+
config.Stats.TrackSingle(WorktreeAddFileFailed, remoteRepository)
259+
return errors.WithStackTrace(addErr)
260+
}
261+
}
262+
}
263+
260264
// With all our untracked files staged, we can now create a commit, passing the All
261265
// option when configuring our commit option so that all modified and deleted files
262266
// will have their changes committed

cmd/repo-operations_test.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package main
22

33
import (
4-
"os"
4+
"bytes"
5+
"github.com/sirupsen/logrus"
6+
"github.com/stretchr/testify/assert"
57
"testing"
68

7-
"github.com/go-git/go-git/v5"
89
"github.com/google/go-github/v32/github"
910
)
1011

@@ -26,25 +27,25 @@ func getMockGithubRepo() *github.Repository {
2627
return repo
2728
}
2829

29-
func cloneLocalTestRepo(t *testing.T) (string, *git.Repository) {
30-
repo := getMockGithubRepo()
30+
// Test that we can execute a script and that the expected stdout and stderr get written to the logger, even if that
31+
// script exits with an error (exit status 1).
32+
func TestExecuteCommandWithLogger(t *testing.T) {
33+
t.Parallel()
3134

32-
config := NewGitXargsTestConfig()
35+
cfg := NewGitXargsConfig()
36+
cfg.Args = []string{"./_testscripts/test-stdout-stderr.sh"}
3337

34-
localPath, localRepo, err := cloneLocalRepository(config, repo)
38+
repo := getMockGithubRepo()
3539

36-
if err != nil {
37-
t.Logf("Could not clone local repo to localPath: %s\n", localPath)
38-
t.Fail()
40+
var buffer bytes.Buffer
41+
logger := &logrus.Logger{
42+
Out: &buffer,
43+
Level: logrus.TraceLevel,
44+
Formatter: new(logrus.TextFormatter),
3945
}
4046

41-
return localPath, localRepo
42-
}
43-
44-
func cleanupLocalTestRepo(t *testing.T, localPath string) error {
45-
removeErr := os.RemoveAll(localPath)
46-
if removeErr != nil {
47-
t.Logf("Error cleaning up test repo at path: %s err: %+v\n", localPath, removeErr)
48-
}
49-
return removeErr
47+
err := executeCommandWithLogger(cfg, ".", repo, logger)
48+
assert.Errorf(t, err, "exit status 1")
49+
assert.Contains(t, buffer.String(), "Hello, from STDOUT")
50+
assert.Contains(t, buffer.String(), "Hello, from STDERR")
5051
}

0 commit comments

Comments
 (0)