1
2
3
4
5
6
7
8 package rename
9
10
11
12
13
14 import (
15 "bytes"
16 "fmt"
17 "go/ast"
18 "go/build"
19 "go/format"
20 "go/token"
21 "log"
22 "os"
23 "os/exec"
24 "path"
25 "path/filepath"
26 "regexp"
27 "runtime"
28 "strconv"
29 "strings"
30 "text/template"
31
32 "golang.org/x/tools/go/buildutil"
33 "golang.org/x/tools/go/loader"
34 "golang.org/x/tools/refactor/importgraph"
35 )
36
37
38
39
40
41
42
43
44
45
46 func Move(ctxt *build.Context, from, to, moveTmpl string) error {
47 srcDir, err := srcDir(ctxt, from)
48 if err != nil {
49 return err
50 }
51
52
53
54 fromDir := buildutil.JoinPath(ctxt, srcDir, filepath.FromSlash(from))
55 toDir := buildutil.JoinPath(ctxt, srcDir, filepath.FromSlash(to))
56 toParent := filepath.Dir(toDir)
57 if !buildutil.IsDir(ctxt, toParent) {
58 return fmt.Errorf("parent directory does not exist for path %s", toDir)
59 }
60
61
62 _, rev, errors := importgraph.Build(ctxt)
63 if len(errors) > 0 {
64
65
66 fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n")
67 for path, err := range errors {
68 fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err)
69 }
70 }
71
72
73
74 affectedPackages := map[string]bool{from: true}
75 destinations := make(map[string]string)
76 for pkg := range subpackages(ctxt, srcDir, from) {
77 for r := range rev[pkg] {
78 affectedPackages[r] = true
79 }
80 destinations[pkg] = strings.Replace(pkg, from, to, 1)
81 }
82
83
84 iprog, err := loadProgram(ctxt, affectedPackages)
85 if err != nil {
86 return err
87 }
88
89
90 var cmd string
91 if moveTmpl != "" {
92 if cmd, err = moveCmd(moveTmpl, fromDir, toDir); err != nil {
93 return err
94 }
95 }
96
97 m := mover{
98 ctxt: ctxt,
99 rev: rev,
100 iprog: iprog,
101 from: from,
102 to: to,
103 fromDir: fromDir,
104 toDir: toDir,
105 affectedPackages: affectedPackages,
106 destinations: destinations,
107 cmd: cmd,
108 }
109
110 if err := m.checkValid(); err != nil {
111 return err
112 }
113
114 m.move()
115
116 return nil
117 }
118
119
120 func srcDir(ctxt *build.Context, pkg string) (string, error) {
121 for _, srcDir := range ctxt.SrcDirs() {
122 path := buildutil.JoinPath(ctxt, srcDir, pkg)
123 if buildutil.IsDir(ctxt, path) {
124 return srcDir, nil
125 }
126 }
127 return "", fmt.Errorf("src dir not found for package: %s", pkg)
128 }
129
130
131
132 func subpackages(ctxt *build.Context, srcDir string, root string) map[string]bool {
133 var subs = make(map[string]bool)
134 buildutil.ForEachPackage(ctxt, func(pkg string, err error) {
135 if err != nil {
136 log.Fatalf("unexpected error in ForEachPackage: %v", err)
137 }
138
139
140 if !(strings.HasPrefix(pkg, root) &&
141 (len(pkg) == len(root) || pkg[len(root)] == '/')) {
142 return
143 }
144
145 p, err := ctxt.Import(pkg, "", build.FindOnly)
146 if err != nil {
147 log.Fatalf("unexpected: package %s can not be located by build context: %s", pkg, err)
148 }
149 if p.SrcRoot == "" {
150 log.Fatalf("unexpected: could not determine srcDir for package %s: %s", pkg, err)
151 }
152 if p.SrcRoot != srcDir {
153 return
154 }
155
156 subs[pkg] = true
157 })
158 return subs
159 }
160
161 type mover struct {
162
163
164 iprog *loader.Program
165 ctxt *build.Context
166
167 rev importgraph.Graph
168
169
170
171 from, to, fromDir, toDir string
172
173
174 affectedPackages map[string]bool
175
176
177 destinations map[string]string
178
179 cmd string
180 }
181
182 func (m *mover) checkValid() error {
183 const prefix = "invalid move destination"
184
185 match, err := regexp.MatchString("^[_\\pL][_\\pL\\p{Nd}]*$", path.Base(m.to))
186 if err != nil {
187 panic("regexp.MatchString failed")
188 }
189 if !match {
190 return fmt.Errorf("%s: %s; gomvpkg does not support move destinations "+
191 "whose base names are not valid go identifiers", prefix, m.to)
192 }
193
194 if buildutil.FileExists(m.ctxt, m.toDir) {
195 return fmt.Errorf("%s: %s conflicts with file %s", prefix, m.to, m.toDir)
196 }
197 if buildutil.IsDir(m.ctxt, m.toDir) {
198 return fmt.Errorf("%s: %s conflicts with directory %s", prefix, m.to, m.toDir)
199 }
200
201 for _, toSubPkg := range m.destinations {
202 if _, err := m.ctxt.Import(toSubPkg, "", build.FindOnly); err == nil {
203 return fmt.Errorf("%s: %s; package or subpackage %s already exists",
204 prefix, m.to, toSubPkg)
205 }
206 }
207
208 return nil
209 }
210
211
212
213 func moveCmd(moveTmpl, fromDir, toDir string) (string, error) {
214 tmpl, err := template.New("movecmd").Parse(moveTmpl)
215 if err != nil {
216 return "", err
217 }
218
219 var buf bytes.Buffer
220 err = tmpl.Execute(&buf, struct {
221 Src string
222 Dst string
223 }{fromDir, toDir})
224 return buf.String(), err
225 }
226
227 func (m *mover) move() error {
228 filesToUpdate := make(map[*ast.File]bool)
229
230
231 pkg, ok := m.iprog.Imported[m.from]
232 if !ok {
233 log.Fatalf("unexpected: package %s is not in import map", m.from)
234 }
235 newName := filepath.Base(m.to)
236 for _, f := range pkg.Files {
237
238 for _, cg := range f.Comments {
239 c := cg.List[0]
240 if c.Slash >= f.Name.End() &&
241 sameLine(m.iprog.Fset, c.Slash, f.Name.End()) &&
242 (f.Decls == nil || c.Slash < f.Decls[0].Pos()) {
243 if strings.HasPrefix(c.Text, `// import "`) {
244 c.Text = `// import "` + m.to + `"`
245 break
246 }
247 if strings.HasPrefix(c.Text, `/* import "`) {
248 c.Text = `/* import "` + m.to + `" */`
249 break
250 }
251 }
252 }
253 f.Name.Name = newName
254 filesToUpdate[f] = true
255 }
256
257
258 for _, info := range m.iprog.Created {
259
260 if info.Pkg.Path() == m.from+"_test" {
261 for _, f := range info.Files {
262 f.Name.Name = newName + "_test"
263 filesToUpdate[f] = true
264 }
265 }
266
267
268
269 for _, imp := range info.Pkg.Imports() {
270 if imp.Path() == m.from {
271 m.affectedPackages[info.Pkg.Path()] = true
272 m.iprog.Imported[info.Pkg.Path()] = info
273 if err := importName(m.iprog, info, m.from, path.Base(m.from), newName); err != nil {
274 return err
275 }
276 }
277 }
278 }
279
280
281
282
283 for p := range m.rev[m.from] {
284 if err := importName(m.iprog, m.iprog.Imported[p], m.from, path.Base(m.from), newName); err != nil {
285 return err
286 }
287 }
288
289
290 for ap := range m.affectedPackages {
291 info, ok := m.iprog.Imported[ap]
292 if !ok {
293 log.Fatalf("unexpected: package %s is not in import map", ap)
294 }
295 for _, f := range info.Files {
296 for _, imp := range f.Imports {
297 importPath, _ := strconv.Unquote(imp.Path.Value)
298 if newPath, ok := m.destinations[importPath]; ok {
299 imp.Path.Value = strconv.Quote(newPath)
300
301 oldName := path.Base(importPath)
302 if imp.Name != nil {
303 oldName = imp.Name.Name
304 }
305
306 newName := path.Base(newPath)
307 if imp.Name == nil && oldName != newName {
308 imp.Name = ast.NewIdent(oldName)
309 } else if imp.Name == nil || imp.Name.Name == newName {
310 imp.Name = nil
311 }
312 filesToUpdate[f] = true
313 }
314 }
315 }
316 }
317
318 for f := range filesToUpdate {
319 var buf bytes.Buffer
320 if err := format.Node(&buf, m.iprog.Fset, f); err != nil {
321 log.Printf("failed to pretty-print syntax tree: %v", err)
322 continue
323 }
324 tokenFile := m.iprog.Fset.File(f.Pos())
325 writeFile(tokenFile.Name(), buf.Bytes())
326 }
327
328
329
330
331
332
333
334
335 if m.cmd != "" {
336
337 var cmd *exec.Cmd
338 switch runtime.GOOS {
339 case "windows":
340 cmd = exec.Command("cmd", "/c", m.cmd)
341 case "plan9":
342 cmd = exec.Command("rc", "-c", m.cmd)
343 default:
344 cmd = exec.Command("sh", "-c", m.cmd)
345 }
346 cmd.Stderr = os.Stderr
347 cmd.Stdout = os.Stdout
348 if err := cmd.Run(); err != nil {
349 return fmt.Errorf("version control system's move command failed: %v", err)
350 }
351
352 return nil
353 }
354
355 return moveDirectory(m.fromDir, m.toDir)
356 }
357
358
359 func sameLine(fset *token.FileSet, x, y token.Pos) bool {
360 return fset.Position(x).Line == fset.Position(y).Line
361 }
362
363 var moveDirectory = func(from, to string) error {
364 return os.Rename(from, to)
365 }
366
View as plain text