Skip to content
Open
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
10 changes: 5 additions & 5 deletions cmd/app/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ func (ar attachmentReader) ReadFile(path string) (io.Reader, error) {
}

data, err := io.ReadAll(file)
closeErr := file.Close()
if err != nil {
return nil, err
}
if closeErr != nil {
return nil, closeErr
}

defer file.Close()

reader := bytes.NewReader(data)

return reader, nil
return bytes.NewReader(data), nil
}

type FileUploader interface {
Expand Down
3 changes: 1 addition & 2 deletions cmd/app/emoji.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,13 @@ func (a emojiService) deleteEmojiFromNote(w http.ResponseWriter, r *http.Request

/* postEmojiOnNote adds an emojis to a note based on the note's ID */
func (a emojiService) postEmojiOnNote(w http.ResponseWriter, r *http.Request) {
defer func() { _ = r.Body.Close() }()
body, err := io.ReadAll(r.Body)
if err != nil {
handleError(w, err, "Could not read request body", http.StatusBadRequest)
return
}

defer r.Body.Close()

var emojiPost CreateNoteEmojiPost
err = json.Unmarshal(body, &emojiPost)

Expand Down
37 changes: 36 additions & 1 deletion cmd/app/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type GitManager interface {
GetProjectUrlFromNativeGitCmd(remote string) (url string, err error)
GetCurrentBranchNameFromNativeGitCmd() (string, error)
GetLatestCommitOnRemote(remote string, branchName string) (string, error)
GetMRHeadCommit(mrIID int64) (string, error)
FetchMRHead(remote string, mrIID int64) error
}

type GitData struct {
Expand Down Expand Up @@ -68,7 +70,7 @@ func NewGitData(remote string, gitlabUrl string, g GitManager) (GitData, error)
// remove part of the hostname from the parsed namespace
url_re := regexp.MustCompile(`[^\/]\/([^\/].*)$`)
url_matches := url_re.FindStringSubmatch(gitlabUrl)
var namespace string = matches[1]
namespace := matches[1]
if len(url_matches) == 2 {
namespace = strings.TrimLeft(strings.TrimPrefix(namespace, url_matches[1]), "/")
}
Expand Down Expand Up @@ -125,6 +127,39 @@ func (g Git) RefreshProjectInfo(remote string) error {
return nil
}

/* Fetches the head ref of a merge request so that fork MR commits are available locally */
func (g Git) FetchMRHead(remote string, mrIID int64) error {
ref := fmt.Sprintf("refs/merge-requests/%d/head", mrIID)
cmd := exec.Command("git", "fetch", remote, fmt.Sprintf("%s:%s", ref, ref))
_, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to fetch MR head ref: %v", err)
}
return nil
}

/*
FetchMrHead fetches the MR head ref when a specific MR is chosen (mrIID != 0).
Returns an error only if the fetch was attempted and failed; returns nil if skipped or succeeded.
*/
func FetchMrHead(g GitManager, remote string, mrIID int64) error {
if mrIID == 0 {
return nil
}
return g.FetchMRHead(remote, mrIID)
}

/* Resolves the commit SHA from a locally stored MR head ref */
func (g Git) GetMRHeadCommit(mrIID int64) (string, error) {
ref := fmt.Sprintf("refs/merge-requests/%d/head", mrIID)
cmd := exec.Command("git", "rev-parse", ref)
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to resolve MR head ref: %v", err)
}
return strings.TrimSpace(string(out)), nil
}

func (g Git) GetLatestCommitOnRemote(remote string, branchName string) (string, error) {
cmd := exec.Command("git", "log", "-1", "--format=%H", fmt.Sprintf("%s/%s", remote, branchName))

Expand Down
121 changes: 121 additions & 0 deletions cmd/app/git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package git

import (
"errors"
"os"
"os/exec"
"strings"
"testing"
)

Expand All @@ -24,6 +27,14 @@ func (f FakeGitManager) GetLatestCommitOnRemote(remote string, branchName string
return "", nil
}

func (f FakeGitManager) GetMRHeadCommit(mrIID int64) (string, error) {
return "", nil
}

func (f FakeGitManager) FetchMRHead(remote string, mrIID int64) error {
return nil
}

func (f FakeGitManager) GetProjectUrlFromNativeGitCmd(string) (url string, err error) {
return f.RemoteUrl, nil
}
Expand Down Expand Up @@ -286,3 +297,113 @@ func TestExtractGitInfo_FailToGetCurrentBranchName(t *testing.T) {
}
})
}

type fetchTrackingManager struct {
fetchCalled bool
fetchErr error
FakeGitManager
}

func (f *fetchTrackingManager) FetchMRHead(remote string, mrIID int64) error {
f.fetchCalled = true
return f.fetchErr
}

func TestFetchMrHead_SkipsWhenMrIIDIsZero(t *testing.T) {
g := &fetchTrackingManager{}
err := FetchMrHead(g, "origin", 0)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if g.fetchCalled {
t.Error("Expected FetchMRHead not to be called when mrIID is 0")
}
}

func TestFetchMrHead_FetchesWhenMrIIDIsNonZero(t *testing.T) {
g := &fetchTrackingManager{}
err := FetchMrHead(g, "origin", 42)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if !g.fetchCalled {
t.Error("Expected FetchMRHead to be called when mrIID is non-zero")
}
}

func TestFetchMrHead_ReturnsErrorOnFetchFailure(t *testing.T) {
g := &fetchTrackingManager{
fetchErr: errors.New("fetch failed"),
}
err := FetchMrHead(g, "origin", 42)
if err == nil {
t.Error("Expected an error, got nil")
}
if err.Error() != "fetch failed" {
t.Errorf("Expected 'fetch failed', got '%s'", err.Error())
}
}

// setupTempGitRepo creates a temp git repo with a commit and an MR head ref,
// changes the working directory to it, and returns the expected commit SHA.
// The caller's working directory is restored via t.Cleanup.
func setupTempGitRepo(t *testing.T, mrIID string) string {
t.Helper()

origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}

tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(origDir) })

cmds := [][]string{
{"git", "init"},
{"git", "config", "user.email", "test@test.com"},
{"git", "config", "user.name", "Test"},
{"git", "commit", "--allow-empty", "-m", "init"},
{"git", "update-ref", "refs/merge-requests/" + mrIID + "/head", "HEAD"},
}
for _, args := range cmds {
cmd := exec.Command(args[0], args[1:]...)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("command %v failed: %v\n%s", args, err, out)
}
}

out, err := exec.Command("git", "rev-parse", "HEAD").Output()
if err != nil {
t.Fatal(err)
}
return strings.TrimSpace(string(out))
}

func TestGetMRHeadCommit_Success(t *testing.T) {
expectedSHA := setupTempGitRepo(t, "42")

g := Git{}
sha, err := g.GetMRHeadCommit(42)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if sha != expectedSHA {
t.Errorf("Expected SHA %s, got %s", expectedSHA, sha)
}
}

func TestGetMRHeadCommit_NonExistentRef(t *testing.T) {
setupTempGitRepo(t, "42")

g := Git{}
_, err := g.GetMRHeadCommit(9999)
if err == nil {
t.Fatal("Expected an error for non-existent ref, got nil")
}
if !strings.Contains(err.Error(), "failed to resolve MR head ref") {
t.Errorf("Expected error about resolving MR head ref, got: %s", err.Error())
}
}
3 changes: 1 addition & 2 deletions cmd/app/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,12 @@ func (a labelService) getLabels(w http.ResponseWriter, r *http.Request) {

func (a labelService) updateLabels(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
defer func() { _ = r.Body.Close() }()
body, err := io.ReadAll(r.Body)
if err != nil {
handleError(w, err, "Could not read request body", http.StatusBadRequest)
return
}

defer r.Body.Close()
var labelUpdateRequest LabelUpdateRequest
err = json.Unmarshal(body, &labelUpdateRequest)

Expand Down
2 changes: 1 addition & 1 deletion cmd/app/list_discussions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestListDiscussions(t *testing.T) {
withMethodCheck(http.MethodPost),
)
data := getDiscussionsList(t, svc, request)
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
assert(t, data.Message, "Discussions retrieved")
assert(t, len(data.Discussions), 2)
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer4")
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer2")
Expand Down
4 changes: 2 additions & 2 deletions cmd/app/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (l LoggingServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {

func logRequest(prefix string, r *http.Request) {
file := openLogFile()
defer file.Close()
defer func() { _ = file.Close() }()
token := r.Header.Get("Private-Token")
r.Header.Set("Private-Token", "REDACTED")
res, err := httputil.DumpRequest(r, true)
Expand All @@ -67,7 +67,7 @@ func logRequest(prefix string, r *http.Request) {

func logResponse(prefix string, r *http.Response) {
file := openLogFile()
defer file.Close()
defer func() { _ = file.Close() }()

res, err := httputil.DumpResponse(r, true)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/app/merge_requests_by_username.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (a mergeRequestListerByUsernameService) getMrs(payload *gitlab.ListProjectM
return []*gitlab.BasicMergeRequest{}, GenericError{endpoint: "/merge_requests_by_username"}
}

defer res.Body.Close()
_ = res.Body.Close()

return mrs, err
}
12 changes: 8 additions & 4 deletions cmd/app/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,16 @@ func (m withMrMiddleware) handle(next http.Handler) http.Handler {
// If the merge request is already attached, skip the middleware logic
if m.data.projectInfo.MergeId == 0 {
options := gitlab.ListProjectMergeRequestsOptions{
Scope: gitlab.Ptr("all"),
SourceBranch: &m.data.gitInfo.BranchName,
Scope: gitlab.Ptr("all"),
}

if pluginOptions.ChosenMrIID != 0 {
// When an MR was explicitly chosen by IID (e.g. via choose_merge_request),
// only filter by IID. Skipping SourceBranch allows fork MRs to be found,
// since their source branch doesn't exist in the local repository.
options.IIDs = gitlab.Ptr([]int64{pluginOptions.ChosenMrIID})
} else {
options.SourceBranch = &m.data.gitInfo.BranchName
}

mergeRequests, _, err := m.client.ListProjectMergeRequests(m.data.projectInfo.ProjectId, &options)
Expand Down Expand Up @@ -175,9 +179,9 @@ func formatValidationErrors(errs validator.ValidationErrors) error {
}
switch e.Tag() {
case "required":
s.WriteString(fmt.Sprintf("%s is required", e.Field()))
fmt.Fprintf(&s, "%s is required", e.Field())
default:
s.WriteString(fmt.Sprintf("The field '%s' failed on validation on the '%s' tag", e.Field(), e.Tag()))
fmt.Fprintf(&s, "The field '%s' failed on validation on the '%s' tag", e.Field(), e.Tag())
}
}

Expand Down
45 changes: 45 additions & 0 deletions cmd/app/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
gitlab "gitlab.com/gitlab-org/api/client-go"
)

type FakePayload struct {
Expand All @@ -14,6 +15,17 @@ type FakePayload struct {

type fakeHandler struct{}

// capturingMergeRequestLister records the options passed to ListProjectMergeRequests
// so tests can verify the filter logic in the withMr middleware.
type capturingMergeRequestLister struct {
capturedOpts *gitlab.ListProjectMergeRequestsOptions
}

func (f *capturingMergeRequestLister) ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.BasicMergeRequest, *gitlab.Response, error) {
f.capturedOpts = opt
return []*gitlab.BasicMergeRequest{{IID: 10}}, makeResponse(http.StatusOK), nil
}

func (f fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
data := SuccessResponse{Message: "Some message"}
Expand Down Expand Up @@ -90,6 +102,39 @@ func TestWithMrMiddleware(t *testing.T) {
assert(t, data.Message, "Multiple MRs found")
assert(t, data.Details, "please call gitlab.choose_merge_request()")
})
t.Run("Filters by IIDs when ChosenMrIID is set", func(t *testing.T) {
pluginOptions.ChosenMrIID = 42
defer func() { pluginOptions.ChosenMrIID = 0 }()

request := makeRequest(t, http.MethodGet, "/foo", nil)
lister := &capturingMergeRequestLister{}
d := data{
projectInfo: &ProjectInfo{},
gitInfo: &git.GitData{BranchName: "foo"},
}
mw := withMr(d, lister)
handler := middleware(fakeHandler{}, mw)
getSuccessData(t, handler, request)

assert(t, (*lister.capturedOpts.IIDs)[0], int64(42))
assert(t, lister.capturedOpts.SourceBranch == nil, true)
})
t.Run("Filters by SourceBranch when ChosenMrIID is not set", func(t *testing.T) {
pluginOptions.ChosenMrIID = 0

request := makeRequest(t, http.MethodGet, "/foo", nil)
lister := &capturingMergeRequestLister{}
d := data{
projectInfo: &ProjectInfo{},
gitInfo: &git.GitData{BranchName: "foo"},
}
mw := withMr(d, lister)
handler := middleware(fakeHandler{}, mw)
getSuccessData(t, handler, request)

assert(t, *lister.capturedOpts.SourceBranch, "foo")
assert(t, lister.capturedOpts.IIDs == nil, true)
})
}

func TestValidatorMiddleware(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion cmd/app/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque
w.Header().Set("Content-Type", "application/json")

commit, err := a.gitService.GetLatestCommitOnRemote(pluginOptions.ConnectionSettings.Remote, a.gitInfo.BranchName)

if err != nil && pluginOptions.ChosenMrIID != 0 {
// Fall back to the MR head ref for fork MRs where the branch doesn't exist on origin
commit, err = a.gitService.GetMRHeadCommit(pluginOptions.ChosenMrIID)
}
if err != nil {
handleError(w, err, "Error getting commit on remote branch", http.StatusInternalServerError)
return
Expand Down
Loading
Loading