Skip to content

Commit 791a5b4

Browse files
authored
Support Tab Completion (#551)
* add tab completion output and make mage -l no longer compile the whole code
1 parent cff82a6 commit 791a5b4

18 files changed

Lines changed: 1528 additions & 318 deletions

File tree

.github/copilot-instructions.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copilot Instructions for Mage
2+
3+
Mage is a make-like build tool that uses Go functions as build targets. Users write plain Go functions in "magefiles" and mage makes them runnable from the command line.
4+
5+
## Build, Test, and Lint
6+
7+
```bash
8+
# Build
9+
go build ./...
10+
11+
# Test (full suite, including race detector as required by CI)
12+
go test -race ./...
13+
14+
# Test a single package
15+
go test -race ./parse/
16+
17+
# Test a single test function
18+
go test -race ./mage/ -run TestGoCmd
19+
20+
# CI runs tests with: go test -v -vet=all -tags CI -race ./...
21+
22+
# Lint (requires golangci-lint) — run after any code changes
23+
golangci-lint run ./...
24+
```
25+
26+
After making changes, always run `golangci-lint run ./...` before committing to catch lint issues early.
27+
28+
Mage builds itself with mage. The bootstrap path (`go run bootstrap.go`) is for building mage when mage isn't installed yet. The project's own build targets live in `magefiles/`.
29+
30+
## Architecture
31+
32+
Mage works by **parsing user Go source files and generating a temporary CLI binary** that dispatches to the user's target functions:
33+
34+
1. **Entry**`main.go` calls `mage.Main()` which parses CLI flags and dispatches commands.
35+
2. **File scanning**`mage/main.go` finds magefiles (files with `//go:build mage` or `// +build mage` tags) in the current directory.
36+
3. **AST parsing**`parse.PrimaryPackage()` uses `go/parser` and `go/doc` to extract exported functions, namespaces (types embedding `mg.Namespace`), `//mage:import` directives, aliases, and the default target.
37+
4. **Code generation**`GenerateMainfile()` renders `mage/magefile_tmpl.go` (a Go `text/template`) into a wrapper `main` package that handles flag parsing, help output, and dispatching to user targets.
38+
5. **Compilation & caching**`Compile()` runs `go build` on the magefiles plus the generated wrapper. The output binary is cached by content hash in the user's cache directory.
39+
40+
### Package Map
41+
42+
- **`mage/`** — Core library: CLI entry point, file scanning, code generation, compilation, and execution. Can be used as a library (`mage.Invoke()`).
43+
- **`mg/`** — User-facing API for magefiles: `Deps`/`CtxDeps` for dependency declaration, `mg.F()` for parameterized targets, `mg.Namespace` for grouping targets, `Fatal`/`Fatalf` for error handling.
44+
- **`parse/`** — Go AST parser that extracts target metadata (functions, namespaces, imports, aliases, defaults) from magefiles into a `parse.PkgInfo` model consumed by code generation.
45+
- **`sh/`** — Shell helper functions (`sh.Run`, `sh.Output`, `sh.Exec`) for use in magefiles.
46+
- **`internal/`** — Shared low-level utilities for command execution and debug output.
47+
- **`target/`** — Timestamp-based rebuild helpers (`target.Path`, `target.Dir`, `target.Glob`) for use in magefiles.
48+
49+
## Key Conventions
50+
51+
- **Zero external dependencies.** Mage uses only the Go standard library. This is intentional — since mage is often vendored into projects, adding dependencies to mage adds them to every project that uses it. Do not add external dependencies.
52+
- **Go 1.18 minimum.** The `go.mod` specifies Go 1.18. CI tests against both Go 1.18 and stable. Avoid language features or stdlib APIs from newer Go versions.
53+
- **Target function signatures** follow strict rules enforced by `parse/parse.go` (`funcType`): optional leading `context.Context` parameter, supported arg types (`string`, `int`, `bool`, `time.Duration`), and must return either nothing or `error`. Pointer args become optional CLI arguments.
54+
- **`//mage:import`** comments on blank imports cause mage to recursively parse imported packages and surface their exported functions as targets.
55+
- **Namespace targets** are methods on types that embed `mg.Namespace`. The type name becomes a CLI prefix (e.g., `mage ns:target`).
56+
- **Documentation** — All functions, methods, types, package variables, and package constants must have Go doc comments describing their purpose, including unexported ones. Every package must have a detailed package-level doc comment explaining what the package is for and how to use it.
57+
- **Formatting** uses `goimports` (configured in `.golangci.toml`).
58+
- **Tests** are primarily integration-style: `mage/main_test.go` calls `Invoke()` against fixture directories under `testdata/`. Table-driven unit tests are used in `parse/`, `sh/`, `internal/`, and `target/`. Always run tests with `-race`.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ Session.vim
3636

3737
# Hugo build lock
3838
.hugo_build.lock
39+
40+
/site/public/
41+
42+
# Release output
43+
/dist

.golangci.toml

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@ disable = [
2121
'asciicheck',
2222
'canonicalheader',
2323
'containedctx',
24-
'copyloopvar', # Not applicable in go versions under 1.22
24+
'copyloopvar',
2525
'cyclop',
2626
'depguard',
2727
'dogsled',
2828
'dupl',
2929
'dupword',
3030
'err113',
31-
'errcheck',
3231
'exhaustive',
3332
'exhaustruct',
3433
'forbidigo',
@@ -61,8 +60,6 @@ disable = [
6160
'thelper',
6261
'unparam',
6362
'varnamelen',
64-
'wastedassign',
65-
'whitespace',
6663
'wrapcheck',
6764
'wsl',
6865
'wsl_v5'
@@ -81,7 +78,6 @@ pattern = 'time.After\.*(# use of time After can create memory allocation issues
8178
[linters.settings.gocritic]
8279
disabled-checks = [
8380
'importShadow',
84-
# 'unnamedResult'
8581
]
8682
enabled-tags = [
8783
'diagnostic',
@@ -98,38 +94,32 @@ sizeThreshold = 256
9894

9995
[linters.settings.gosec]
10096
excludes = [
101-
'G204',
102-
'G304',
103-
'G307',
104-
'G702',
105-
'G706'
97+
'G204', # Audit use of exec.Command with variable arguments (command injection risk)
98+
'G304', # Audit file path provided as taint input (path traversal via user-supplied file paths)
99+
'G307', # Deferring a method which returns an error (e.g., defer f.Close() without checking the error)
100+
'G702', # net/http SetDeadline not called (HTTP server timeout not configured)
101+
'G703', # Path traversal via taint analysis
102+
'G706', # Audit use of io.ReadAll (potential denial of service from unbounded reads)
106103
]
107104

108-
[linters.settings.gosec.config]
109-
[linters.settings.gosec.config.G104]
110-
# os = ['Setenv']
111-
112105
[linters.settings.govet]
113106
disable = [
107+
'fieldalignment',
114108
'shadow',
115-
'fieldalignment'
116109
]
117110
enable-all = true
118111

119112
[linters.settings.modernize]
120113
disable = ["any"]
121114

122115
[linters.settings.nestif]
123-
min-complexity = 9
116+
min-complexity = 6
124117

125118
[linters.settings.nolintlint]
126119
require-explanation = true
127120
require-specific = true
128121
allow-unused = false
129122

130-
# [linters.settings.recvcheck]
131-
# exclusions = ['*.UnmarshalJSON']
132-
133123
[linters.settings.revive]
134124
confidence = 0.8
135125
severity = 'error'
@@ -140,7 +130,6 @@ name = 'comment-spacings'
140130
arguments = [
141131
'nolint'
142132
]
143-
disabled = false
144133

145134
[[linters.settings.revive.rules]]
146135
name = 'argument-limit'
@@ -168,34 +157,18 @@ disabled = true
168157
name = 'flag-parameter'
169158
disabled = true
170159

171-
[[linters.settings.revive.rules]]
172-
name = 'blank-imports'
173-
disabled = false
174-
175160
[[linters.settings.revive.rules]]
176161
name = 'cognitive-complexity'
177162
disabled = true
178163

179-
[[linters.settings.revive.rules]]
180-
name = 'constant-logical-expr'
181-
disabled = false
182-
183164
[[linters.settings.revive.rules]]
184165
name = 'cyclomatic'
185166
disabled = true
186167

187-
[[linters.settings.revive.rules]]
188-
name = 'file-header'
189-
disabled = false
190-
191168
[[linters.settings.revive.rules]]
192169
name = 'function-length'
193170
disabled = true
194171

195-
[[linters.settings.revive.rules]]
196-
name = 'get-return'
197-
disabled = false
198-
199172
[[linters.settings.revive.rules]]
200173
name = 'line-length-limit'
201174
disabled = true
@@ -204,10 +177,6 @@ disabled = true
204177
name = 'max-public-structs'
205178
disabled = true
206179

207-
[[linters.settings.revive.rules]]
208-
name = 'optimize-operands-order'
209-
disabled = false
210-
211180
[[linters.settings.revive.rules]]
212181
name = 'redundant-test-main-exit'
213182
disabled = true

bootstrap.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
//go:build ignore
2-
// +build ignore
32

43
package main
54

install_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
//go:build CI
2-
// +build CI
32

43
package main
54

mage/colors.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package mage
2+
3+
//nolint:revive // these are named this way because we also use this code in the generated output and we don't want the imports to potentially conflict with globals in user code.
4+
import (
5+
_fmt "fmt"
6+
_os "os"
7+
_strconv "strconv"
8+
_strings "strings"
9+
)
10+
11+
var printName = func(str string) string {
12+
// color is ANSI color type
13+
type color int
14+
15+
// If you add/change/remove any items in this constant,
16+
// you will need to run "stringer -type=color" in this directory again.
17+
// NOTE: Please keep the list in an alphabetical order.
18+
const (
19+
black color = iota
20+
red
21+
green
22+
yellow
23+
blue
24+
magenta
25+
cyan
26+
white
27+
brightblack
28+
brightred
29+
brightgreen
30+
brightyellow
31+
brightblue
32+
brightmagenta
33+
brightcyan
34+
brightwhite
35+
)
36+
37+
// AnsiColor are ANSI color codes for supported terminal colors.
38+
var ansiColor = map[color]string{
39+
black: "\u001b[30m",
40+
red: "\u001b[31m",
41+
green: "\u001b[32m",
42+
yellow: "\u001b[33m",
43+
blue: "\u001b[34m",
44+
magenta: "\u001b[35m",
45+
cyan: "\u001b[36m",
46+
white: "\u001b[37m",
47+
brightblack: "\u001b[30;1m",
48+
brightred: "\u001b[31;1m",
49+
brightgreen: "\u001b[32;1m",
50+
brightyellow: "\u001b[33;1m",
51+
brightblue: "\u001b[34;1m",
52+
brightmagenta: "\u001b[35;1m",
53+
brightcyan: "\u001b[36;1m",
54+
brightwhite: "\u001b[37;1m",
55+
}
56+
57+
const colorName = "blackredgreenyellowbluemagentacyanwhitebrightblackbrightredbrightgreenbrightyellowbrightbluebrightmagentabrightcyanbrightwhite"
58+
59+
var colorIndex = [...]uint8{0, 5, 8, 13, 19, 23, 30, 34, 39, 50, 59, 70, 82, 92, 105, 115, 126}
60+
61+
colorToLowerString := func(i color) string {
62+
if i < 0 || i >= color(len(colorIndex)-1) {
63+
return "color(" + _strconv.FormatInt(int64(i), 10) + ")"
64+
}
65+
return colorName[colorIndex[i]:colorIndex[i+1]]
66+
}
67+
68+
// ansiColorReset is an ANSI color code to reset the terminal color.
69+
const ansiColorReset = "\033[0m"
70+
71+
// defaultTargetAnsiColor is a default ANSI color for colorizing targets.
72+
// It is set to Cyan as an arbitrary color, because it has a neutral meaning
73+
var defaultTargetAnsiColor = ansiColor[cyan]
74+
75+
getAnsiColor := func(color string) (string, bool) {
76+
colorLower := _strings.ToLower(color)
77+
for k, v := range ansiColor {
78+
colorConstLower := colorToLowerString(k)
79+
if colorConstLower == colorLower {
80+
return v, true
81+
}
82+
}
83+
return "", false
84+
}
85+
86+
// Terminals which don't support color:
87+
//
88+
// TERM=vt100
89+
// TERM=cygwin
90+
// TERM=xterm-mono
91+
var noColorTerms = map[string]bool{
92+
"vt100": false,
93+
"cygwin": false,
94+
"xterm-mono": false,
95+
}
96+
97+
// terminalSupportsColor checks if the current console supports color output
98+
//
99+
// Supported:
100+
//
101+
// linux, mac, or windows's ConEmu, Cmder, putty, git-bash.exe, pwsh.exe
102+
//
103+
// Not supported:
104+
//
105+
// windows cmd.exe, powerShell.exe
106+
terminalSupportsColor := func() bool {
107+
envTerm := _os.Getenv("TERM")
108+
if _, ok := noColorTerms[envTerm]; ok {
109+
return false
110+
}
111+
return true
112+
}
113+
114+
// enableColor reports whether the user has requested to enable a color output.
115+
enableColor := func() bool {
116+
b, _ := _strconv.ParseBool(_os.Getenv("MAGEFILE_ENABLE_COLOR"))
117+
return b
118+
}
119+
120+
// targetColor returns the ANSI color which should be used to colorize targets.
121+
targetColor := func() string {
122+
s, exists := _os.LookupEnv("MAGEFILE_TARGET_COLOR")
123+
if exists {
124+
if c, ok := getAnsiColor(s); ok {
125+
return c
126+
}
127+
}
128+
return defaultTargetAnsiColor
129+
}
130+
131+
// store the color terminal variables, so that the detection isn't repeated for each target
132+
var enableColorValue = enableColor() && terminalSupportsColor()
133+
var targetColorValue = targetColor()
134+
135+
if enableColorValue {
136+
return _fmt.Sprintf("%s%s%s", targetColorValue, str, ansiColorReset)
137+
}
138+
139+
return str
140+
}

mage/command_string.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)