diff --git a/go.mod b/go.mod index fac7c248..1768ad26 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/markbates/goth v1.78.0 github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.8.4 + github.com/urfave/cli/v2 v2.27.1 github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark-emoji v1.0.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc @@ -45,6 +46,7 @@ require ( github.com/blevesearch/zapx/v13 v13.3.10 // indirect github.com/blevesearch/zapx/v14 v14.3.10 // indirect github.com/blevesearch/zapx/v15 v15.3.13 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect @@ -72,8 +74,10 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.etcd.io/bbolt v1.3.8 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect diff --git a/go.sum b/go.sum index 2bcabc93..d508981d 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -291,6 +293,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -304,10 +308,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/actions/actions.go b/internal/actions/actions.go index 629cf3b8..404ae66f 100644 --- a/internal/actions/actions.go +++ b/internal/actions/actions.go @@ -17,12 +17,12 @@ type ActionStatus struct { } const ( - SyncReposFromFS = iota - SyncReposFromDB = iota - GitGcRepos = iota - SyncGistPreviews = iota - ResetHooks = iota - IndexGists = iota + SyncReposFromFS = iota + SyncReposFromDB + GitGcRepos + SyncGistPreviews + ResetHooks + IndexGists ) var ( diff --git a/internal/cli/hook.go b/internal/cli/hook.go new file mode 100644 index 00000000..a05ff36b --- /dev/null +++ b/internal/cli/hook.go @@ -0,0 +1,38 @@ +package cli + +import ( + "github.com/thomiceli/opengist/internal/hooks" + "github.com/urfave/cli/v2" + "os" +) + +var CmdHook = cli.Command{ + Name: "hook", + Usage: "Run Git server hooks, used and should only be called by Opengist itself", + Subcommands: []*cli.Command{ + &CmdHookPreReceive, + &CmdHookPostReceive, + }, +} + +var CmdHookPreReceive = cli.Command{ + Name: "pre-receive", + Usage: "Run Git server pre-receive hook for a repository", + Action: func(ctx *cli.Context) error { + if err := hooks.PreReceive(os.Stdin, os.Stdout, os.Stderr); err != nil { + os.Exit(1) + } + return nil + }, +} + +var CmdHookPostReceive = cli.Command{ + Name: "post-receive", + Usage: "Run Git server post-receive hook for a repository", + Action: func(ctx *cli.Context) error { + if err := hooks.PostReceive(os.Stdin, os.Stdout, os.Stderr); err != nil { + os.Exit(1) + } + return nil + }, +} diff --git a/internal/cli/main.go b/internal/cli/main.go new file mode 100644 index 00000000..702211da --- /dev/null +++ b/internal/cli/main.go @@ -0,0 +1,131 @@ +package cli + +import ( + "fmt" + "github.com/rs/zerolog/log" + "github.com/thomiceli/opengist/internal/config" + "github.com/thomiceli/opengist/internal/db" + "github.com/thomiceli/opengist/internal/git" + "github.com/thomiceli/opengist/internal/index" + "github.com/thomiceli/opengist/internal/memdb" + "github.com/thomiceli/opengist/internal/ssh" + "github.com/thomiceli/opengist/internal/web" + "github.com/urfave/cli/v2" + "os" + "path" + "path/filepath" +) + +var CmdVersion = cli.Command{ + Name: "version", + Usage: "Print the version of Opengist", + Action: func(c *cli.Context) error { + fmt.Println("Opengist v" + config.OpengistVersion) + return nil + }, +} + +var CmdStart = cli.Command{ + Name: "start", + Usage: "Start Opengist server", + Action: func(ctx *cli.Context) error { + Initialize(ctx) + go web.NewServer(os.Getenv("OG_DEV") == "1").Start() + go ssh.Start() + select {} + }, +} + +var ConfigFlag = cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "Path to a config file in YAML format", +} + +func App() error { + app := cli.NewApp() + app.Name = "Opengist" + app.Usage = "A self-hosted pastebin powered by Git." + app.HelpName = "opengist" + + app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook} + app.DefaultCommand = CmdStart.Name + app.Flags = []cli.Flag{ + &ConfigFlag, + } + return app.Run(os.Args) +} + +func Initialize(ctx *cli.Context) { + fmt.Println("Opengist v" + config.OpengistVersion) + + if err := config.InitConfig(ctx.String("config")); err != nil { + panic(err) + } + if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil { + panic(err) + } + + config.InitLog() + + gitVersion, err := git.GetGitVersion() + if err != nil { + log.Fatal().Err(err).Send() + } + + if ok, err := config.CheckGitVersion(gitVersion); err != nil { + log.Fatal().Err(err).Send() + } else if !ok { + log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.28 and some features would not work. " + + "Current git version: " + gitVersion) + } + + homePath := config.GetHomeDir() + log.Info().Msg("Data directory: " + homePath) + + if err := createSymlink(); err != nil { + log.Fatal().Err(err).Send() + } + + if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil { + log.Fatal().Err(err).Send() + } + if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil { + log.Fatal().Err(err).Send() + } + if err := os.MkdirAll(filepath.Join(homePath, "custom"), 0755); err != nil { + log.Fatal().Err(err).Send() + } + log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename)) + if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil { + log.Fatal().Err(err).Msg("Failed to initialize database") + } + + if err := memdb.Setup(); err != nil { + log.Fatal().Err(err).Msg("Failed to initialize in memory database") + } + + if config.C.IndexEnabled { + log.Info().Msg("Index directory: " + filepath.Join(homePath, config.C.IndexDirname)) + if err := index.Open(filepath.Join(homePath, config.C.IndexDirname)); err != nil { + log.Fatal().Err(err).Msg("Failed to open index") + } + } +} + +func createSymlink() error { + exePath, err := os.Executable() + if err != nil { + return err + } + + symlinkPath := path.Join(config.GetHomeDir(), "opengist-bin") + + if _, err := os.Lstat(symlinkPath); err == nil { + if err := os.Remove(symlinkPath); err != nil { + return err + } + } + + return os.Symlink(exePath, symlinkPath) +} diff --git a/internal/config/config.go b/internal/config/config.go index e15e775a..5f3e77fd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -121,6 +121,10 @@ func InitConfig(configPath string) error { C = c + if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil { + return err + } + return nil } diff --git a/internal/db/gist.go b/internal/db/gist.go index 4b526c87..f975926f 100644 --- a/internal/db/gist.go +++ b/internal/db/gist.go @@ -5,7 +5,6 @@ import ( "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/lexers" "github.com/dustin/go-humanize" - "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" "github.com/thomiceli/opengist/internal/index" "os/exec" @@ -330,10 +329,6 @@ func (gist *Gist) InitRepository() error { return git.InitRepository(gist.User.Username, gist.Uuid) } -func (gist *Gist) InitRepositoryViaInit(ctx echo.Context) error { - return git.InitRepositoryViaInit(gist.User.Username, gist.Uuid, ctx) -} - func (gist *Gist) DeleteRepository() error { return git.DeleteRepository(gist.User.Username, gist.Uuid) } diff --git a/internal/db/sshkey.go b/internal/db/sshkey.go index ee62151c..e79d9739 100644 --- a/internal/db/sshkey.go +++ b/internal/db/sshkey.go @@ -19,7 +19,7 @@ type SSHKey struct { User User `validate:"-" ` } -func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error { +func (sshKey *SSHKey) BeforeCreate(*gorm.DB) error { pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content)) if err != nil { return err diff --git a/internal/git/commands.go b/internal/git/commands.go index e4530c2b..294d500f 100644 --- a/internal/git/commands.go +++ b/internal/git/commands.go @@ -24,6 +24,7 @@ var ( ) const truncateLimit = 2 << 18 +const BaseHash = "0000000000000000000000000000000000000000" type RevisionNotFoundError struct{} @@ -80,16 +81,6 @@ func InitRepository(user string, gist string) error { return CreateDotGitFiles(user, gist) } -func InitRepositoryViaInit(user string, gist string, ctx echo.Context) error { - repositoryPath := RepositoryPath(user, gist) - - if err := InitRepository(user, gist); err != nil { - return err - } - repositoryUrl := RepositoryUrl(ctx, user, gist) - return createDotGitHookFile(repositoryPath, "post-receive", fmt.Sprintf(postReceive, repositoryUrl, repositoryUrl)) -} - func CountCommits(user string, gist string) (string, error) { repositoryPath := RepositoryPath(user, gist) @@ -424,7 +415,6 @@ func Push(gistTmpId string) error { if err != nil { return err } - return os.RemoveAll(tmpRepositoryPath) } @@ -534,8 +524,12 @@ func CreateDotGitFiles(user string, gist string) error { } defer f1.Close() - if err = createDotGitHookFile(repositoryPath, "pre-receive", preReceive); err != nil { - return err + if os.Getenv("OPENGIST_SKIP_GIT_HOOKS") != "1" { + for _, hook := range []string{"pre-receive", "post-receive"} { + if err = createDotGitHookFile(repositoryPath, hook, fmt.Sprintf(hookTemplate, hook)); err != nil { + return err + } + } } return nil @@ -570,57 +564,6 @@ func removeFilesExceptGit(dir string) error { }) } -const preReceive = `#!/bin/sh - -disallowed_files="" - -while read -r old_rev new_rev ref -do - if [ "$old_rev" = "0000000000000000000000000000000000000000" ]; then - # This is the first commit, so we check all the files in that commit - changed_files=$(git ls-tree -r --name-only "$new_rev") - else - # This is not the first commit, so we compare it with its predecessor - changed_files=$(git diff --name-only "$old_rev" "$new_rev") - fi - - while IFS= read -r file - do - case $file in - */*) - disallowed_files="${disallowed_files}${file} " - ;; - esac - done </dev/null; then - git symbolic-ref HEAD "$refname" - fi -done - -echo "" -echo "Your new repository has been created here: %s" -echo "" -echo "If you want to keep working with your gist, you could set the remote URL via:" -echo "git remote set-url origin %s" -echo "" - -rm -f $0 +const hookTemplate = `#!/bin/sh +"$OG_OPENGIST_HOME_INTERNAL/opengist-bin" hook %s ` diff --git a/internal/git/commands_test.go b/internal/git/commands_test.go index 23c08850..32e3723a 100644 --- a/internal/git/commands_test.go +++ b/internal/git/commands_test.go @@ -1,11 +1,11 @@ package git import ( - "github.com/labstack/echo/v4" + "bytes" + "fmt" "github.com/stretchr/testify/require" "github.com/thomiceli/opengist/internal/config" - "net/http" - "net/http/httptest" + "github.com/thomiceli/opengist/internal/hooks" "os" "os/exec" "path" @@ -15,6 +15,8 @@ import ( ) func setup(t *testing.T) { + _ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1") + err := config.InitConfig("") require.NoError(t, err, "Could not init config") @@ -30,7 +32,7 @@ func setup(t *testing.T) { } func teardown(t *testing.T) { - err := os.RemoveAll(path.Join(config.C.OpengistHome, "tests")) + err := os.RemoveAll(path.Join(config.GetHomeDir(), "tests")) require.NoError(t, err, "Could not remove repos directory") } @@ -44,9 +46,6 @@ func TestInitDeleteRepository(t *testing.T) { require.NoError(t, err, "Could not run git command") require.Equal(t, "true", strings.TrimSpace(string(out)), "Repository is not bare") - _, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "hooks", "pre-receive")) - require.NoError(t, err, "pre-receive hook not found") - _, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "git-daemon-export-ok")) require.NoError(t, err, "git-daemon-export-ok file not found") @@ -247,30 +246,6 @@ func TestTruncate(t *testing.T) { require.Equal(t, 2, len(content), "Content size is not correct") } -func TestInitViaGitInit(t *testing.T) { - setup(t) - defer teardown(t) - - e := echo.New() - - // Create a mock HTTP request - req := httptest.NewRequest(http.MethodPost, "/", nil) - - // Create a mock HTTP response recorder - rec := httptest.NewRecorder() - - // Create a new Echo context - c := e.NewContext(req, rec) - - // Define your user and gist - user := "testUser" - gist := "testGist" - - err := InitRepositoryViaInit(user, gist, c) - - require.NoError(t, err) -} - func TestGitInitBranchNames(t *testing.T) { setup(t) defer teardown(t) @@ -292,21 +267,67 @@ func TestGitInitBranchNames(t *testing.T) { require.Equal(t, "refs/heads/main", strings.TrimSpace(string(out)), "Repository should have main branch as default") } +func TestPreReceiveHook(t *testing.T) { + setup(t) + defer teardown(t) + var lastCommitHash string + err := os.Chdir(RepositoryPath("thomas", "gist1")) + require.NoError(t, err, "Could not change directory") + + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_file.txt": "some allowed file", + "my_file2.txt": "some allowed file\nagain", + }) + lastCommitHash = lastHashOfCommit(t, "thomas", "gist1") + err = hooks.PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr) + require.NoError(t, err, "Should not have an error on pre-receive hook for commit+push 1") + + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_file.txt": "some allowed file", + "dir/my_file.txt": "some disallowed file suddenly", + }) + lastCommitHash = lastHashOfCommit(t, "thomas", "gist1") + err = hooks.PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr) + require.Error(t, err, "Should have an error on pre-receive hook for commit+push 2") + require.Equal(t, "pushing files in directories is not allowed: [dir/my_file.txt]", err.Error(), "Error message is not correct") + + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_file.txt": "some allowed file", + "dir/ok/afileagain.txt": "some disallowed file\nagain", + }) + lastCommitHash = lastHashOfCommit(t, "thomas", "gist1") + err = hooks.PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr) + require.Error(t, err, "Should have an error on pre-receive hook for commit+push 3") + require.Equal(t, "pushing files in directories is not allowed: [dir/ok/afileagain.txt dir/my_file.txt]", err.Error(), "Error message is not correct") + + commitToBare(t, "thomas", "gist1", map[string]string{ + "allowedfile.txt": "some allowed file only", + }) + lastCommitHash = lastHashOfCommit(t, "thomas", "gist1") + err = hooks.PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr) + require.Error(t, err, "Should have an error on pre-receive hook for commit+push 4") + require.Equal(t, "pushing files in directories is not allowed: [dir/ok/afileagain.txt dir/my_file.txt]", err.Error(), "Error message is not correct") + + _ = os.Chdir(os.TempDir()) // Leave the current dir to avoid errors on teardown +} + func commitToBare(t *testing.T, user string, gist string, files map[string]string) { err := CloneTmp(user, gist, gist, "thomas@mail.com", true) - require.NoError(t, err, "Could not commit to repository") + require.NoError(t, err, "Could not clone repository") if len(files) > 0 { for filename, content := range files { - if err := SetFileContent(gist, filename, content); err != nil { - require.NoError(t, err, "Could not commit to repository") + if strings.Contains(filename, "/") { + dir := filepath.Dir(filename) + err := os.MkdirAll(filepath.Join(TmpRepositoryPath(gist), dir), os.ModePerm) + require.NoError(t, err, "Could not create directory") } + _ = os.WriteFile(filepath.Join(TmpRepositoryPath(gist), filename), []byte(content), 0644) if err := AddAll(gist); err != nil { - require.NoError(t, err, "Could not commit to repository") + require.NoError(t, err, "Could not add all to repository") } } - } if err := CommitRepository(gist, user, "thomas@mail.com"); err != nil { @@ -314,6 +335,14 @@ func commitToBare(t *testing.T, user string, gist string, files map[string]strin } if err := Push(gist); err != nil { - require.NoError(t, err, "Could not commit to repository") + require.NoError(t, err, "Could not push to repository") } } + +func lastHashOfCommit(t *testing.T, user string, gist string) string { + cmd := exec.Command("git", "rev-parse", "HEAD") + cmd.Dir = RepositoryPath(user, gist) + out, err := cmd.Output() + require.NoError(t, err, "Could not run git command") + return strings.TrimSpace(string(out)) +} diff --git a/internal/hooks/post-receive.go b/internal/hooks/post-receive.go new file mode 100644 index 00000000..58f15e46 --- /dev/null +++ b/internal/hooks/post-receive.go @@ -0,0 +1,43 @@ +package hooks + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +func PostReceive(in io.Reader, out, er io.Writer) error { + scanner := bufio.NewScanner(in) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Fields(line) + if len(parts) != 3 { + _, _ = fmt.Fprintln(er, "Invalid input") + return fmt.Errorf("invalid input") + } + oldrev, _, refname := parts[0], parts[1], parts[2] + + if err := verifyHEAD(); err != nil { + setSymbolicRef(refname) + } + + if oldrev == BaseHash { + _, _ = fmt.Fprintf(out, "\nYour new repository has been created here: %s\n\n", os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL")) + _, _ = fmt.Fprintln(out, "If you want to keep working with your gist, you could set the remote URL via:") + _, _ = fmt.Fprintf(out, "git remote set-url origin %s\n\n", os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL")) + } + } + + return nil +} + +func verifyHEAD() error { + return exec.Command("git", "rev-parse", "--verify", "--quiet", "HEAD").Run() +} + +func setSymbolicRef(refname string) { + _ = exec.Command("git", "symbolic-ref", "HEAD", refname).Run() +} diff --git a/internal/hooks/pre-receive.go b/internal/hooks/pre-receive.go new file mode 100644 index 00000000..343f7956 --- /dev/null +++ b/internal/hooks/pre-receive.go @@ -0,0 +1,80 @@ +package hooks + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os/exec" + "strings" +) + +const BaseHash = "0000000000000000000000000000000000000000" + +func PreReceive(in io.Reader, out, er io.Writer) error { + var err error + var disallowedFiles []string + var disallowedCommits []string + + scanner := bufio.NewScanner(in) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, " ") + if len(parts) < 3 { + _, _ = fmt.Fprintln(er, "Invalid input") + return fmt.Errorf("invalid input") + } + + oldRev, newRev := parts[0], parts[1] + + var changedFiles string + if oldRev == BaseHash { + // First commit + if changedFiles, err = getChangedFiles(newRev); err != nil { + _, _ = fmt.Fprintln(er, "Failed to get changed files") + return err + } + } else { + if changedFiles, err = getChangedFiles(fmt.Sprintf("%s..%s", oldRev, newRev)); err != nil { + _, _ = fmt.Fprintln(er, "Failed to get changed files") + return err + } + } + + var currentCommit string + for _, file := range strings.Fields(changedFiles) { + if strings.HasPrefix(file, "/") { + currentCommit = file[1:] + } + + if strings.Contains(file[1:], "/") { + disallowedFiles = append(disallowedFiles, file) + disallowedCommits = append(disallowedCommits, currentCommit[0:7]) + } + } + } + + if len(disallowedFiles) > 0 { + _, _ = fmt.Fprintln(out, "\nPushing files in directories is not allowed:") + for i := range disallowedFiles { + _, _ = fmt.Fprintf(out, " %s (%s)\n", disallowedFiles[i], disallowedCommits[i]) + } + _, _ = fmt.Fprintln(out) + return fmt.Errorf("pushing files in directories is not allowed: %s", disallowedFiles) + } + + return nil +} + +func getChangedFiles(rev string) (string, error) { + cmd := exec.Command("git", "log", "--name-only", "--format=/%H", "--diff-filter=AM", rev) + + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return "", err + } + + return out.String(), nil +} diff --git a/internal/web/git_http.go b/internal/web/git_http.go index 40c0f781..1f7974cc 100644 --- a/internal/web/git_http.go +++ b/internal/web/git_http.go @@ -134,7 +134,7 @@ func gitHttp(ctx echo.Context) error { gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1) gist.Title = "gist:" + gist.Uuid - if err = gist.InitRepositoryViaInit(ctx); err != nil { + if err = gist.InitRepository(); err != nil { return errorRes(500, "Cannot init repository in the file system", err) } @@ -193,6 +193,7 @@ func pack(ctx echo.Context, serviceType string) error { } repositoryPath := getData(ctx, "repositoryPath").(string) + gist := getData(ctx, "gist").(*db.Gist) var stderr bytes.Buffer cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath) @@ -200,13 +201,15 @@ func pack(ctx echo.Context, serviceType string) error { cmd.Stdin = reqBody cmd.Stdout = ctx.Response().Writer cmd.Stderr = &stderr + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "OPENGIST_REPOSITORY_URL_INTERNAL="+git.RepositoryUrl(ctx, gist.User.Username, gist.Identifier())) + if err = cmd.Run(); err != nil { return errorRes(500, "Cannot run git "+serviceType+" ; "+stderr.String(), err) } // updatedAt is updated only if serviceType is receive-pack if serviceType == "receive-pack" { - gist := getData(ctx, "gist").(*db.Gist) if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil { return err diff --git a/internal/web/settings.go b/internal/web/settings.go index 69cdbb95..8d1f5834 100644 --- a/internal/web/settings.go +++ b/internal/web/settings.go @@ -163,8 +163,8 @@ func usernameProcess(ctx echo.Context) error { return redirect(ctx, "/settings") } - sourceDir := filepath.Join(config.C.OpengistHome, git.ReposDirectory, strings.ToLower(user.Username)) - destinationDir := filepath.Join(config.C.OpengistHome, git.ReposDirectory, strings.ToLower(dto.Username)) + sourceDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(user.Username)) + destinationDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(dto.Username)) if _, err := os.Stat(sourceDir); !os.IsNotExist(err) { err := os.Rename(sourceDir, destinationDir) diff --git a/internal/web/test/server.go b/internal/web/test/server.go index d9514262..1f3d7091 100644 --- a/internal/web/test/server.go +++ b/internal/web/test/server.go @@ -125,6 +125,8 @@ func structToURLValues(s interface{}) url.Values { } func setup(t *testing.T) { + _ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1") + err := config.InitConfig("") require.NoError(t, err, "Could not init config") @@ -159,7 +161,10 @@ func teardown(t *testing.T, s *testServer) { err := db.Close() require.NoError(t, err, "Could not close database") - err = os.RemoveAll(path.Join(config.C.OpengistHome, "tests")) + err = os.RemoveAll(path.Join(config.GetHomeDir(), "tests")) + require.NoError(t, err, "Could not remove repos directory") + + err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "repos")) require.NoError(t, err, "Could not remove repos directory") // err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex")) diff --git a/opengist.go b/opengist.go index 956e2c71..ccc80ab9 100644 --- a/opengist.go +++ b/opengist.go @@ -1,81 +1,12 @@ package main import ( - "flag" - "fmt" - "github.com/rs/zerolog/log" - "github.com/thomiceli/opengist/internal/config" - "github.com/thomiceli/opengist/internal/db" - "github.com/thomiceli/opengist/internal/git" - "github.com/thomiceli/opengist/internal/index" - "github.com/thomiceli/opengist/internal/memdb" - "github.com/thomiceli/opengist/internal/ssh" - "github.com/thomiceli/opengist/internal/web" + "github.com/thomiceli/opengist/internal/cli" "os" - "path/filepath" ) -func initialize() { - fmt.Println("Opengist v" + config.OpengistVersion) - - configPath := flag.String("config", "", "Path to a config file in YML format") - flag.Parse() - - if err := config.InitConfig(*configPath); err != nil { - panic(err) - } - if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil { - panic(err) - } - - config.InitLog() - - gitVersion, err := git.GetGitVersion() - if err != nil { - log.Fatal().Err(err).Send() - } - - if ok, err := config.CheckGitVersion(gitVersion); err != nil { - log.Fatal().Err(err).Send() - } else if !ok { - log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.28 and some features would not work. " + - "Current git version: " + gitVersion) - } - - homePath := config.GetHomeDir() - log.Info().Msg("Data directory: " + homePath) - - if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil { - log.Fatal().Err(err).Send() - } - if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil { - log.Fatal().Err(err).Send() - } - if err := os.MkdirAll(filepath.Join(homePath, "custom"), 0755); err != nil { - log.Fatal().Err(err).Send() - } - log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename)) - if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil { - log.Fatal().Err(err).Msg("Failed to initialize database") - } - - if err := memdb.Setup(); err != nil { - log.Fatal().Err(err).Msg("Failed to initialize in memory database") - } - - if config.C.IndexEnabled { - log.Info().Msg("Index directory: " + filepath.Join(homePath, config.C.IndexDirname)) - if err := index.Open(filepath.Join(homePath, config.C.IndexDirname)); err != nil { - log.Fatal().Err(err).Msg("Failed to open index") - } - } -} - func main() { - initialize() - - go web.NewServer(os.Getenv("OG_DEV") == "1").Start() - go ssh.Start() - - select {} + if err := cli.App(); err != nil { + os.Exit(1) + } }