mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 12:51:55 +00:00
Now that we require go1.18, we can use a builtin type alias for
"interface{}": "any". I've updated all code except lib, which can be
imported by external modules. That is currently pinned at go1.16.
Additionally I had to rerun generate since rewriting generated go code
will fail CI.
find -name '*.go' | xargs gofmt -s -r 'interface{} -> any' -w
git checkout lib
go generate ./...
Test Plan: CI will exercise that the code still compiles. Otherwise this
is a noop.
286 lines
7.2 KiB
Go
286 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"html/template"
|
|
"io/fs"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"github.com/sourcegraph/sourcegraph/lib/errors"
|
|
)
|
|
|
|
type Serve struct {
|
|
Addr string
|
|
Root string
|
|
Info *log.Logger
|
|
Debug *log.Logger
|
|
|
|
// updatingServerInfo is used to ensure we only have 1 goroutine running
|
|
// git update-server-info.
|
|
updatingServerInfo uint64
|
|
}
|
|
|
|
func (s *Serve) Start() error {
|
|
ln, err := net.Listen("tcp", s.Addr)
|
|
if err != nil {
|
|
return errors.Wrap(err, "listen")
|
|
}
|
|
|
|
// Update Addr to what listener actually used.
|
|
s.Addr = ln.Addr().String()
|
|
|
|
s.Info.Printf("listening on http://%s", s.Addr)
|
|
h := s.handler()
|
|
|
|
if err := (&http.Server{Handler: h}).Serve(ln); err != nil {
|
|
return errors.Wrap(err, "serving")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var indexHTML = template.Must(template.New("").Parse(`<html>
|
|
<head><title>src-expose</title></head>
|
|
<body>
|
|
<h2>src-expose</h2>
|
|
<pre>
|
|
{{.Explain}}
|
|
<ul>{{range .Links}}
|
|
<li><a href="{{.}}">{{.}}</a></li>
|
|
{{- end}}
|
|
</ul>
|
|
</pre>
|
|
</body>
|
|
</html>`))
|
|
|
|
type Repo struct {
|
|
Name string
|
|
URI string
|
|
}
|
|
|
|
func (s *Serve) handler() http.Handler {
|
|
s.Info.Printf("serving git repositories from %s", s.Root)
|
|
s.configureRepos()
|
|
|
|
// Start the HTTP server.
|
|
mux := &http.ServeMux{}
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
err := indexHTML.Execute(w, map[string]any{
|
|
"Explain": explainAddr(s.Addr),
|
|
"Links": []string{
|
|
"/v1/list-repos",
|
|
"/repos/",
|
|
},
|
|
})
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
})
|
|
|
|
mux.HandleFunc("/v1/list-repos", func(w http.ResponseWriter, _ *http.Request) {
|
|
var repos []Repo
|
|
var reposRootIsRepo bool
|
|
for _, name := range s.configureRepos() {
|
|
if name == "." {
|
|
reposRootIsRepo = true
|
|
}
|
|
|
|
repos = append(repos, Repo{
|
|
Name: name,
|
|
URI: path.Join("/repos", name),
|
|
})
|
|
}
|
|
|
|
if reposRootIsRepo {
|
|
// Update all names to be relative to the parent of
|
|
// reposRoot. This is to give a better name than "." for repos
|
|
// root
|
|
abs, err := filepath.Abs(s.Root)
|
|
if err != nil {
|
|
http.Error(w, "failed to get the absolute path of reposRoot: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rootName := filepath.Base(abs)
|
|
for i := range repos {
|
|
repos[i].Name = path.Join(rootName, repos[i].Name)
|
|
}
|
|
}
|
|
|
|
resp := struct {
|
|
Items []Repo
|
|
}{
|
|
Items: repos,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
_ = enc.Encode(&resp)
|
|
})
|
|
|
|
mux.Handle("/repos/", http.StripPrefix("/repos/", http.FileServer(httpDir{http.Dir(s.Root)})))
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.Contains(r.URL.Path, "/.git/objects/") { // exclude noisy path
|
|
s.Info.Printf("%s %s", r.Method, r.URL.Path)
|
|
}
|
|
mux.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
type httpDir struct {
|
|
http.Dir
|
|
}
|
|
|
|
// Wraps the http.Dir to inject subdir "/.git" to the path.
|
|
func (d httpDir) Open(name string) (http.File, error) {
|
|
// Backwards compatibility for old config, skip if name already contains "/.git/".
|
|
if !strings.Contains(name, "/.git/") {
|
|
// Loops over subpaths that are requested by Git client to find the insert point.
|
|
// The order of slice matters, must try to match "/objects/" before "/info/"
|
|
// because there is a path "/objects/info/" exists.
|
|
for _, sp := range []string{"/objects/", "/info/", "/HEAD"} {
|
|
if i := strings.LastIndex(name, sp); i > 0 {
|
|
name = name[:i] + "/.git" + name[i:]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return d.Dir.Open(name)
|
|
}
|
|
|
|
// configureRepos finds all .git directories and configures them to be served.
|
|
// It returns a slice of all the git directories it finds. The paths are
|
|
// relative to root.
|
|
func (s *Serve) configureRepos() []string {
|
|
var gitDirs []string
|
|
|
|
err := filepath.Walk(s.Root, func(path string, fi fs.FileInfo, fileErr error) error {
|
|
if fileErr != nil {
|
|
s.Info.Printf("WARN: ignoring error searching %s: %v", path, fileErr)
|
|
return nil
|
|
}
|
|
if !fi.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// We recurse into bare repositories to find subprojects. Prevent
|
|
// recursing into .git
|
|
if filepath.Base(path) == ".git" {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
// Check whether a particular directory is a repository or not.
|
|
//
|
|
// A directory which also is a repository (have .git folder inside it)
|
|
// will contain nil error. If it does, proceed to configure.
|
|
gitdir := filepath.Join(path, ".git")
|
|
if fi, err := os.Stat(gitdir); err != nil || !fi.IsDir() {
|
|
s.Debug.Printf("not a repository root: %s", path)
|
|
return nil
|
|
}
|
|
|
|
if err := configurePostUpdateHook(s.Info, gitdir); err != nil {
|
|
s.Info.Printf("failed configuring repo at %s: %v", gitdir, err)
|
|
return nil
|
|
}
|
|
|
|
subpath, err := filepath.Rel(s.Root, path)
|
|
if err != nil {
|
|
// According to WalkFunc docs, path is always filepath.Join(root,
|
|
// subpath). So Rel should always work.
|
|
s.Info.Fatalf("filepath.Walk returned %s which is not relative to %s: %v", path, s.Root, err)
|
|
}
|
|
gitDirs = append(gitDirs, filepath.ToSlash(subpath))
|
|
|
|
// Check whether a repository is a bare repository or not.
|
|
//
|
|
// If it yields false, which means it is a non-bare repository,
|
|
// skip the directory so that it will not recurse to the subdirectories.
|
|
// If it is a bare repository, proceed to recurse.
|
|
c := exec.Command("git", "rev-parse", "--is-bare-repository")
|
|
c.Dir = gitdir
|
|
out, _ := c.CombinedOutput()
|
|
|
|
if string(out) == "false\n" {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
// Our WalkFunc doesn't return any errors, so neither should filepath.Walk
|
|
panic(err)
|
|
}
|
|
|
|
go s.allUpdateServerInfo(gitDirs)
|
|
|
|
return gitDirs
|
|
}
|
|
|
|
// allUpdateServerInfo will run updateServerInfo on each gitDirs. To prevent
|
|
// too many of these processes running, it will only run one at a time.
|
|
func (s *Serve) allUpdateServerInfo(gitDirs []string) {
|
|
if !atomic.CompareAndSwapUint64(&s.updatingServerInfo, 0, 1) {
|
|
return
|
|
}
|
|
|
|
for _, dir := range gitDirs {
|
|
gitdir := filepath.Join(s.Root, dir)
|
|
if err := updateServerInfo(gitdir); err != nil {
|
|
s.Info.Printf("git update-server-info failed for %s: %v", gitdir, err)
|
|
}
|
|
}
|
|
|
|
atomic.StoreUint64(&s.updatingServerInfo, 0)
|
|
}
|
|
|
|
var postUpdateHook = []byte("#!/bin/sh\nexec git update-server-info\n")
|
|
|
|
// configureOneRepos tweaks a .git repo such that it can be git cloned.
|
|
// See https://theartofmachinery.com/2016/07/02/git_over_http.html
|
|
// for background.
|
|
func configurePostUpdateHook(logger *log.Logger, gitDir string) error {
|
|
postUpdatePath := filepath.Join(gitDir, "hooks", "post-update")
|
|
if b, _ := os.ReadFile(postUpdatePath); bytes.Equal(b, postUpdateHook) {
|
|
return nil
|
|
}
|
|
|
|
logger.Printf("configuring git post-update hook for %s", gitDir)
|
|
|
|
if err := updateServerInfo(gitDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(postUpdatePath), 0755); err != nil {
|
|
return errors.Wrap(err, "create git hooks dir")
|
|
}
|
|
if err := os.WriteFile(postUpdatePath, postUpdateHook, 0755); err != nil {
|
|
return errors.Wrap(err, "setting post-update hook")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updateServerInfo(gitDir string) error {
|
|
c := exec.Command("git", "update-server-info")
|
|
c.Dir = gitDir
|
|
out, err := c.CombinedOutput()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "updating server info: %s", out)
|
|
}
|
|
return nil
|
|
}
|