Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ <h3 class="mt1 f6 lh-title" id="parse.numthreads">
<div>
<h3 class="mt1 f6 lh-title" id="parse.gitfunctions">GitFunctions</h3>
<p>
Activates built-in functions git_branch, git_commit, git_show and git_state. If disabled they will not be usable at parse time. Defaults to true.
Activates built-in functions git_branch, git_commit, git_show, git_state and git_tags. If disabled they will not be usable at parse time. Defaults to true.
</p>
</div>
</li>
Expand Down
15 changes: 15 additions & 0 deletions docs/lexicon.html
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,21 @@ <h3 class="title-3" id="git_state">
</table>
</div>
</section>

<section class="mt4">
<h3 class="title-3" id="git_tags">
git_tags
</h3>

<code class="code-signature">git_tags()</code>

<p>
Returns the list of tags that point to the current commit by running
<code class="code">git tag -l --points-at HEAD</code>. The returned list
is sorted in lexicographical order. If no tags point to the current
commit, an empty list is returned.
</p>
</section>
</section>


Expand Down
2 changes: 2 additions & 0 deletions rules/builtins.build_defs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ def git_show(fmt:str) -> str:
fail('Disabled in config')
def git_state(clean_label:str="clean", dirty_label:str="dirty") -> str:
fail('Disabled in config')
def git_tags() -> list:
fail("Disabled in config")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate you're following the existing pattern, but do you think we should be explicit here about it being parse.GitFunctions in the config?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - I'll do that for all of them in a follow-up PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def debug(args):
pass
Expand Down
2 changes: 1 addition & 1 deletion src/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ type Configuration struct {
PreloadSubincludes []BuildLabel `help:"Subinclude targets to preload by the parser before loading any BUILD files.\nSubincludes can be slow so it's recommended to use PreloadBuildDefs where possible." example:"///pleasings//python:requirements"`
BuildDefsDir []string `help:"Directory to look in when prompted for help topics that aren't known internally." example:"build_defs"`
NumThreads int `help:"Number of parallel parse operations to run.\nIs overridden by the --num_threads command line flag." example:"6"`
GitFunctions bool `help:"Activates built-in functions git_branch, git_commit, git_show and git_state. If disabled they will not be usable at parse time."`
GitFunctions bool `help:"Activates built-in functions git_branch, git_commit, git_show, git_state and git_tags. If disabled they will not be usable at parse time."`
} `help:"The [parse] section in the config contains settings specific to parsing files."`
Display struct {
UpdateTitle bool `help:"Updates the title bar of the shell window Please is running in as the build progresses. This isn't on by default because not everyone's shell is configured to reset it again after and we don't want to alter it forever."`
Expand Down
1 change: 1 addition & 0 deletions src/parse/asp/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func registerBuiltins(s *scope) {
setNativeCode(s, "git_commit", execGitCommit)
setNativeCode(s, "git_show", execGitShow)
setNativeCode(s, "git_state", execGitState)
setNativeCode(s, "git_tags", execGitTags)
}
setLogCode(s, "debug", log.Debug)
setLogCode(s, "info", log.Info)
Expand Down
96 changes: 70 additions & 26 deletions src/parse/asp/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,26 @@ func init() {
execPromises = make(map[execKey]*execPromise, initCacheSize)
}

// doExec fork/exec's a command and returns the output as a string. exec
// accepts either a string or a list of commands and arguments. The output from
// exec() is memoized by default to prevent side effects and aid in performance
// of duplicate calls to the same command with the same arguments (e.g. `git
// rev-parse --short HEAD`). The output from exec()'ed commands must be
// reproducible. If storeNegative is true, it is possible for success to return
// successfully and return an error (i.e. we're expecing a command to fail and
// want to cache the failure).
// doExec executes a command and returns what it outputted on stdout.
//
// NOTE: Commands that rely on the current working directory must not be cached.
func doExec(s *scope, cmdIn pyObject, cacheOutput bool, storeNegative bool) (pyObj pyObject, success bool, err error) {
// cmdIn is the command to execute followed by the command's arguments, either as a list or a
// space-delimited string.
//
// If cacheOutput is true, the command's output is memoised to prevent side effects and improve the
// performance of duplicate calls to the same command with the same arguments (e.g. `git rev-parse
// --short HEAD`); to gain any benefit from this, the output from executed commands must be
// reproducible.
//
// If storeNegative is true, the command's output will be memoised (and doExec will return with
// success = true) even if the command exits with a non-zero exit code; this is useful when
// a command is expected to fail.
//
// If outputAsList is true, pyObj will be a pyList consisting of one pyStr per line of the command's
// output (i.e. split on newlines); if outputAsList is false, pyObj will be the command's entire
// output in a single pyStr.
//
// NOTE: Commands that rely on the current working directory must not have their output cached.
func doExec(s *scope, cmdIn pyObject, cacheOutput, storeNegative, outputAsList bool) (pyObj pyObject, success bool, err error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not suggesting you fix this now, but why on earth does this take a pyObject? As far as I can tell, it is only used with hard-coded lists of commands...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess is that this function was originally written with the intention of it being exposed directly to ASP in future, but that (thankfully) never happened.

var argv []string
if isType(cmdIn, "str") {
argv = strings.Fields(string(cmdIn.(pyString)))
Expand Down Expand Up @@ -97,7 +106,7 @@ func doExec(s *scope, cmdIn pyObject, cacheOutput bool, storeNegative bool) (pyO
// since we're also returning an error, which tells the caller to
// fallthrough their logic if a command returns with a non-zero exit code.
outStr = execSetCachedOutput(key, argv, &execOut{out: outStr, success: false})
return pyString(outStr), true, err
return parseExecOutput(outStr, outputAsList), true, err
}

return pyString(fmt.Sprintf("exec() unable to run command %q: %v", argv, err)), false, err
Expand All @@ -107,7 +116,7 @@ func doExec(s *scope, cmdIn pyObject, cacheOutput bool, storeNegative bool) (pyO
outStr = execSetCachedOutput(key, argv, &execOut{out: outStr, success: true})
}

return pyString(outStr), true, nil
return parseExecOutput(outStr, outputAsList), true, nil
}

// execFindCmd looks for a command using PATH and returns a cached abspath.
Expand Down Expand Up @@ -174,6 +183,22 @@ func execGetCachedOutput(key execKey, args []string) (output *execOut, found boo
return outputRaw.(*execOut), true
}

// parseExecOutput returns the given command output as a pyList consisting of one pyStr per line if
// asList is true, or as a pyStr containing the entire command output if asList is false.
func parseExecOutput(output string, asList bool) pyObject {
if !asList {
return pyString(output)
}
// We don't know how many lines the command output consists of, but 8 seems like a reasonable
// initial upper limit, given that this function is only used to parse the output of git(1)
// commands.
ret := make([]pyObject, 0, 8)
for line := range strings.SplitSeq(output, "\n") {
ret = append(ret, pyString(line))
}
return pyList(ret)
}

// execGitBranch returns the output of a git_branch() command.
//
// git_branch() returns the output of `git symbolic-ref -q --short HEAD`
Expand All @@ -187,9 +212,7 @@ func execGitBranch(s *scope, args []pyObject) pyObject {
}
cmdIn = append(cmdIn, pyString("HEAD"))

cacheOutput := true
storeNegative := true
gitSymRefResult, success, err := doExec(s, pyList(cmdIn), cacheOutput, storeNegative)
gitSymRefResult, success, err := doExec(s, pyList(cmdIn), true, true, false)
switch {
case success && err == nil:
return gitSymRefResult
Expand All @@ -207,8 +230,7 @@ func execGitBranch(s *scope, args []pyObject) pyObject {
cmdIn[1] = pyString("show")
cmdIn[2] = pyString("-q")
cmdIn[3] = pyString("--format=%D")
storeNegative = false
gitShowResult, success, err := doExec(s, pyList(cmdIn), cacheOutput, storeNegative)
gitShowResult, success, err := doExec(s, pyList(cmdIn), true, false, false)
if !success {
// doExec returns a formatted error string
return s.Error("exec() %q failed: %v", pyList(cmdIn).String(), err)
Expand All @@ -233,10 +255,8 @@ func execGitCommit(s *scope, args []pyObject) pyObject {
pyString("HEAD"),
}

cacheOutput := true
storeNegative := false
// No error handling required since we don't want to retry
pyResult, success, err := doExec(s, pyList(cmdIn), cacheOutput, storeNegative)
pyResult, success, err := doExec(s, pyList(cmdIn), true, false, false)
if !success {
return s.Error("git_commit() failed: %v", err)
}
Expand Down Expand Up @@ -291,9 +311,7 @@ func execGitShow(s *scope, args []pyObject) pyObject {
pyString(fmt.Sprintf("--format=%s", formatVerb)),
}

cacheOutput := true
storeNegative := false
pyResult, success, err := doExec(s, pyList(cmdIn), cacheOutput, storeNegative)
pyResult, success, err := doExec(s, pyList(cmdIn), true, false, false)
if !success {
return s.Error("git_show() failed: %v", err)
}
Expand All @@ -313,9 +331,7 @@ func execGitState(s *scope, args []pyObject) pyObject {
pyString("--porcelain"),
}

cacheOutput := true
storeNegative := false
pyResult, success, err := doExec(s, pyList(cmdIn), cacheOutput, storeNegative)
pyResult, success, err := doExec(s, pyList(cmdIn), true, false, false)
if !success {
return s.Error("git_state() failed: %v", err)
}
Expand All @@ -330,6 +346,34 @@ func execGitState(s *scope, args []pyObject) pyObject {
return cleanLabel
}

// execGitTags implements the built-in git_tags function, which returns the output of
// `git tag -l --sort=refname --points-at HEAD`. Tag names are returned in lexicographical order.
func execGitTags(s *scope, args []pyObject) pyObject {
cmdIn := []pyObject{
pyString("git"),
pyString("tag"),
pyString("-l"),
pyString("--sort=refname"),
pyString("--points-at"),
pyString("HEAD"),
}

// No special error handling required here - `git tag` exits with exit code 0 even if no tags
// point at HEAD.
pyResult, success, err := doExec(s, pyList(cmdIn), true, false, true)
Comment thread
chrisnovakovic marked this conversation as resolved.
if !success {
return s.Error("git_tags() failed: %v", err)
}

// If no tags point at HEAD, return an empty list, rather than the list consisting of a single
// empty string element that doExec returned to us.
if list, ok := pyResult.(pyList); ok && list.Len() == 1 && list.Item(0) == pyString("") {
return pyList{}
}

return pyResult
}

// execMakeKey returns an execKey.
func execMakeKey(args []string) execKey {
return execKey{
Expand Down
Loading