archive.go
⎇
Raw
1package gitcmd
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9
10 "github.com/klauspost/compress/zstd"
11)
12
13// Archive writes <slug>-<ref>.zip, .tar.gz and .tar.zst into outDir.
14// Concurrency is bounded by MaxConcurrentArchives, because each job forks git
15// and streams the whole tree.
16func (g *Git) Archive(ctx context.Context, name, ref, slug, outDir string) error {
17 p, err := g.repoDir(name)
18 if err != nil {
19 return err
20 }
21 if !ValidRef(ref) {
22 return fmt.Errorf("%q: %w", ref, ErrInvalidRef)
23 }
24 select {
25 case g.archiveSem <- struct{}{}:
26 defer func() { <-g.archiveSem }()
27 case <-ctx.Done():
28 return ctx.Err()
29 }
30
31 base := filepath.Join(outDir, slug+"-"+ref)
32 for _, f := range []struct{ format, ext string }{
33 {"zip", ".zip"},
34 {"tar.gz", ".tar.gz"},
35 } {
36 if _, err := g.run(ctx, runOpts{}, "-C", p, "archive",
37 "--format="+f.format, "--output="+base+f.ext, "--end-of-options", ref); err != nil {
38 return fmt.Errorf("archive %s: %w", f.format, err)
39 }
40 }
41 return g.archiveZstd(ctx, p, ref, base+".tar.zst")
42}
43
44// archiveZstd compresses `git archive --format=tar` in-process, so the
45// container needs no zstd binary. A failure must not leave a truncated
46// artifact, so the partial file is removed.
47func (g *Git) archiveZstd(ctx context.Context, repo, ref, outPath string) error {
48 cmd := exec.CommandContext(ctx, "git", "-C", repo, "archive", "--format=tar", "--end-of-options", ref)
49 cmd.Env = g.env
50 stdout, err := cmd.StdoutPipe()
51 if err != nil {
52 return err
53 }
54 f, err := os.Create(outPath)
55 if err != nil {
56 return err
57 }
58 err = func() error {
59 defer f.Close()
60 if err := cmd.Start(); err != nil {
61 return err
62 }
63 enc, err := zstd.NewWriter(f)
64 if err != nil {
65 _ = cmd.Wait()
66 return err
67 }
68 // Copy the whole stream before Wait, otherwise git blocks on a full pipe.
69 _, copyErr := enc.ReadFrom(stdout)
70 closeErr := enc.Close()
71 waitErr := cmd.Wait()
72 if copyErr != nil {
73 return copyErr
74 }
75 if closeErr != nil {
76 return closeErr
77 }
78 return waitErr
79 }()
80 if err != nil {
81 os.Remove(outPath)
82 return fmt.Errorf("archive tar.zst: %w", err)
83 }
84 return nil
85}
86