1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func backup(root, dir string, keep int) error {
if strings.HasSuffix(dir, ".db") {
return mustStore(root).Backup(dir)
}
if err := os.MkdirAll(filepath.Join(dir, "repos"), 0o750); err != nil {
return err
}
snapshot := filepath.Join(dir, "gitpocket-"+time.Now().UTC().Format("2006-01-02")+".db")
os.Remove(snapshot)
if err := mustStore(root).Backup(snapshot); err != nil {
return err
}
old, _ := filepath.Glob(filepath.Join(dir, "gitpocket-*.db"))
for i := 0; i+keep < len(old); i++ {
os.Remove(old[i])
}
var failed []string
repos, _ := filepath.Glob(filepath.Join(root, "repos", "*", "*.git"))
live := map[string]bool{}
for _, repo := range repos {
rel, _ := filepath.Rel(filepath.Join(root, "repos"), repo)
out := filepath.Join(dir, "repos", strings.TrimSuffix(rel, ".git")+".bundle")
live[out] = true
if info, err := os.Stat(out); err == nil && !repoUpdated(repo).After(info.ModTime()) {
continue
}
if err := bundle(repo, out); err != nil {
failed = append(failed, rel+": "+err.Error())
}
}
stale, _ := filepath.Glob(filepath.Join(dir, "repos", "*", "*.bundle"))
for _, b := range stale {
if !live[b] {
os.Remove(b)
}
}
if err := copyAttachments(filepath.Join(root, "attachments"), filepath.Join(dir, "attachments")); err != nil {
failed = append(failed, "attachments: "+err.Error())
}
if len(failed) > 0 {
return fmt.Errorf("could not bundle %s", strings.Join(failed, "; "))
}
return nil
}
func bundle(repo, out string) error {
g := Git{Dir: repo}
if _, err := g.run(nil, "rev-parse", "--verify", "--quiet", "HEAD"); err != nil {
if refs, _ := g.run(nil, "for-each-ref", "--count=1"); len(refs) == 0 {
return nil
}
}
if err := os.MkdirAll(filepath.Dir(out), 0o750); err != nil {
return err
}
tmp := out + ".tmp"
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, "git", "-C", repo, "bundle", "create", "--quiet", tmp, "--all")
cmd.Env = []string{"GIT_CONFIG_NOSYSTEM=1", "HOME=/nonexistent", "PATH=" + os.Getenv("PATH")}
if out, err := cmd.CombinedOutput(); err != nil {
os.Remove(tmp)
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
}
return os.Rename(tmp, out)
}
func copyAttachments(src, dst string) error {
files, err := os.ReadDir(src)
if err != nil || len(files) == 0 {
return nil
}
if err := os.MkdirAll(dst, 0o750); err != nil {
return err
}
for _, f := range files {
out := filepath.Join(dst, f.Name())
if !attachName.MatchString(f.Name()) {
continue
}
if _, err := os.Stat(out); err == nil {
continue
}
data, err := os.ReadFile(filepath.Join(src, f.Name()))
if err == nil {
err = os.WriteFile(out, data, 0o640)
}
if err != nil {
return err
}
}
return nil
}
|