forked from sei-protocol/sei-tendermint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
linkpatch.go
205 lines (177 loc) · 5.43 KB
/
linkpatch.go
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// Program linkpatch rewrites absolute URLs pointing to targets in GitHub in
// Markdown link tags to target a different branch.
//
// This is used to update documentation links for backport branches.
// See https://github.com/ari-anchor/sei-tendermint/issues/7675 for context.
package main
import (
"bytes"
"flag"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/creachadair/atomicfile"
)
var (
repoName = flag.String("repo", "tendermint/tendermint", "Repository name to match")
sourceBranch = flag.String("source", "master", "Source branch name (required)")
targetBranch = flag.String("target", "", "Target branch name (required)")
doRecur = flag.Bool("recur", false, "Recur into subdirectories")
skipPath stringList
skipMatch regexpFlag
// Match markdown links pointing to absolute URLs.
// This only works for "inline" links, not referenced links.
// The submetch selects the URL.
linkRE = regexp.MustCompile(`(?m)\[.*?\]\((https?://.*?)\)`)
)
func init() {
flag.Var(&skipPath, "skip-path", "Skip these paths (comma-separated)")
flag.Var(&skipMatch, "skip-match", "Skip URLs matching this regexp (RE2)")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `Usage: %[1]s [options] <file/dir>...
Rewrite absolute Markdown links targeting the specified GitHub repository
and source branch name to point to the target branch instead. Matching
files are updated in-place.
Each path names either a directory to list, or a single file path to
rewrite. By default, only the top level of a directory is scanned; use -recur
to recur into subdirectories.
Options:
`, filepath.Base(os.Args[0]))
flag.PrintDefaults()
}
}
func main() {
flag.Parse()
switch {
case *repoName == "":
log.Fatal("You must specify a non-empty -repo name (org/repo)")
case *targetBranch == "":
log.Fatal("You must specify a non-empty -target branch")
case *sourceBranch == "":
log.Fatal("You must specify a non-empty -source branch")
case *sourceBranch == *targetBranch:
log.Fatalf("Source and target branch are the same (%q)", *sourceBranch)
case flag.NArg() == 0:
log.Fatal("You must specify at least one file/directory to rewrite")
}
r, err := regexp.Compile(fmt.Sprintf(`^https?://github.com/%s/(?:blob|tree)/%s`,
*repoName, *sourceBranch))
if err != nil {
log.Fatalf("Compiling regexp: %v", err)
}
for _, path := range flag.Args() {
if err := processPath(r, path); err != nil {
log.Fatalf("Processing %q failed: %v", path, err)
}
}
}
func processPath(r *regexp.Regexp, path string) error {
fi, err := os.Lstat(path)
if err != nil {
return err
}
if fi.Mode().IsDir() {
return processDir(r, path)
} else if fi.Mode().IsRegular() {
return processFile(r, path)
}
return nil // nothing to do with links, device files, sockets, etc.
}
func processDir(r *regexp.Regexp, root string) error {
return filepath.Walk(root, func(path string, fi fs.FileInfo, err error) error {
if err != nil {
return err
}
if fi.IsDir() {
if skipPath.Contains(path) {
log.Printf("Skipping %q (per -skip-path)", path)
return filepath.SkipDir // explicitly skipped
} else if !*doRecur && path != root {
return filepath.SkipDir // skipped because we aren't recurring
}
return nil // nothing else to do for directories
} else if skipPath.Contains(path) {
log.Printf("Skipping %q (per -skip-path)", path)
return nil // explicitly skipped
} else if filepath.Ext(path) != ".md" {
return nil // nothing to do for non-Markdown files
}
return processFile(r, path)
})
}
func processFile(r *regexp.Regexp, path string) error {
log.Printf("Processing file %q", path)
input, err := os.ReadFile(path)
if err != nil {
return err
}
pos := 0
var output bytes.Buffer
for _, m := range linkRE.FindAllSubmatchIndex(input, -1) {
href := string(input[m[2]:m[3]])
u := r.FindStringIndex(href)
if u == nil || skipMatch.MatchString(href) {
if u != nil {
log.Printf("Skipped URL %q (by -skip-match)", href)
}
output.Write(input[pos:m[1]]) // copy the existing data as-is
pos = m[1]
continue
}
// Copy everything before the URL as-is, then write the replacement.
output.Write(input[pos:m[2]]) // everything up to the URL
fmt.Fprintf(&output, `https://github.com/%s/blob/%s%s`, *repoName, *targetBranch, href[u[1]:])
// Write out the tail of the match, everything after the URL.
output.Write(input[m[3]:m[1]])
pos = m[1]
}
output.Write(input[pos:]) // the rest of the file
_, err = atomicfile.WriteAll(path, &output, 0644)
return err
}
// stringList implements the flag.Value interface for a comma-separated list of strings.
type stringList []string
func (lst *stringList) Set(s string) error {
if s == "" {
*lst = nil
} else {
*lst = strings.Split(s, ",")
}
return nil
}
// Contains reports whether lst contains s.
func (lst stringList) Contains(s string) bool {
for _, elt := range lst {
if s == elt {
return true
}
}
return false
}
func (lst stringList) String() string { return strings.Join([]string(lst), ",") }
// regexpFlag implements the flag.Value interface for a regular expression.
type regexpFlag struct{ *regexp.Regexp }
func (r regexpFlag) MatchString(s string) bool {
if r.Regexp == nil {
return false
}
return r.Regexp.MatchString(s)
}
func (r *regexpFlag) Set(s string) error {
c, err := regexp.Compile(s)
if err != nil {
return err
}
r.Regexp = c
return nil
}
func (r regexpFlag) String() string {
if r.Regexp == nil {
return ""
}
return r.Regexp.String()
}