git repo url is appended with /git-upload-pack on using go-git's Plainclone function

293 views Asked by At

Trying to clone a repo from Azure devops.

func (gitopt *GitOptions) clone() (*git.Repository, error) {
    r, err := git.PlainClone(gitopt.RepoDir, false, &git.CloneOptions{
        Progress: os.Stdout,
        URL:      "https://<path to repo>",
        Auth: &http.BasicAuth{
            Username: "gituser",
            Password: gitopt.PAT,
        },
    })
    if err != nil {
        log.Info(err.Error())
        return nil, err
    }

    return r, nil
}

Running this code is adding /git-upload-pack at the end of repo url ("https://<path to repo>/git-upload-pack") due to which clone is failing with status code 400. Couldn't understand why this is being appended.

2

There are 2 answers

0
bk2204 On BEST ANSWER

The Git protocol over HTTP consists of two steps, which vary depending on the version of the protocol in use. In v0 and v1, the first request is to /info/refs and reads the refs in use, and then the second request is to either /git-upload-pack (for fetches and clones) or /git-receive-pack (for pushes). In v2, the endpoints are the same, but the first is a capability request, and then the ref request and data transfer are made to the second endpoint.

In all of these cases, the URL that you've provided is merely a base from which the paths are appended. The different paths make access control easier for simple Git servers behind something like nginx or Apache, which is why there's not just a single URL component.

So the URL that's being generated is in fact correct. The reason you're seeing a 400 is because of an issue in which Azure DevOps requires clients to support the multi_ack capability, which go-git does not. While technically servers don't have to provide support to any clients they don't want to, the Git smart HTTP protocol is typically designed to degrade gracefully, so it isn't a safe assumption that a client will necessarily support any specific set of features, and Azure DevOps should avoid making that assumption.

The linked issue has a link to a pull request that fixes the problem in some, but not all, cases. You may need to update to a later version to take advantage of that, though.

1
FlorianBATTESTI On

I am experiencing the same issues. go-git does not work with Azure DevOps repositories. Therefore, I tried to find a workaround and discovered libgit2.

You can perhaps try this: sudo apt-get install libgit2-dev However, for me, the version installed by my Linux was not compatible with the go module So I installed libgit2 on my linux with these commands:

git clone --branch v1.5.0 https://github.com/libgit2/libgit2.git
cd libgit2/
cmake .
cmake --build .
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH

Then, I simply installed this module:

go get github.com/libgit2/git2go/v34

After that, I was able to clone my Azure DevOps repository using this code:

package main

import (
    "log"

    git "github.com/libgit2/git2go/v34"
)

func main() {
    repoAUrl := "https://dev.azure.com/organisation/projet/_git/name_repository"
    localPath := "name_local_folder"
    branchName := "origin/develop" // Replace with your branch
    token := "123456"             // Replace with your PAT

    // Setting up authentication callbacks
    callbacks := git.RemoteCallbacks{
        CredentialsCallback: func(url string, usernameFromURL string, allowedTypes git.CredType) (*git.Credential, error) {
            cred, err := git.NewCredentialUserpassPlaintext("your_username", token)
            if err != nil {
                return nil, err
            }
            return cred, nil
        },
    }

    // Clone options
    cloneOpts := git.CloneOptions{
        FetchOptions: git.FetchOptions{
            RemoteCallbacks: callbacks,
        },
    }

    // Cloning the repository
    repo, err := git.Clone(repoAUrl, localPath, &cloneOpts)
    if err != nil {
        log.Fatal(err)
    }

    // Searching for the "develop" branch
    localBranch, err := repo.LookupBranch(branchName, git.BranchRemote)
    if err != nil {
        log.Fatalf("Error when searching for 'develop' branch: %v", err)
    }

    // Setting checkout options
    checkoutOpts := &git.CheckoutOpts{
        Strategy: git.CheckoutForce, // Use CheckoutSafe to avoid overwriting local changes
    }

    // Updating the HEAD of the repo to the branch target
    err = repo.SetHeadDetached(localBranch.Target())
    if err != nil {
        log.Fatalf("Error when updating the HEAD of the repo: %v", err)
    }

    // Checking out the HEAD of the repo
    err = repo.CheckoutHead(checkoutOpts)
    if err != nil {
        log.Fatalf("Error during the checkout of HEAD: %v", err)
    }

    log.Println("Cloning and checkout of 'develop' branch completed successfully.")
}

I hope my workaround can help someone.