///|
/// Build and count a complete document. Remote inputs are fetched and cleaned here.
/// The caller decides where to write the result.
pub async fn pack(options : Options, runtime : Runtime) -> PackResult {
if options.max_file_size <= 0 ||
options.max_git_bytes <= 0 ||
options.include_logs_count <= 0 ||
options.include_logs_count > 10000 ||
options.encoding != "o200k_base" ||
(options.token_budget is Some(n) && n <= 0) ||
(options.remote is None && options.remote_branch is Some(_)) {
raise ConfigurationError
}
match options.remote {
None => pack_directory(options, runtime, Json::null())
Some(url) => {
if options.directory != "." {
raise ConfigurationError
}
let history = if options.include_logs {
options.include_logs_count
} else {
0
}
let checkout = try {
match options.remote_branch {
None => @git.checkout(url, history~)
Some(revision) => @git.checkout(url, revision~, history~)
}
} catch {
err => {
if @async.is_cancellation_error(err) {
raise err
}
raise RemoteError
}
}
defer @git.cleanup(checkout)
let local_options = {
..options,
directory: checkout.directory,
remote: None,
remote_branch: None,
}
pack_directory(local_options, runtime, {
"kind": "remote",
"url": checkout.url,
"revision": checkout.revision,
})
}
}
}
///|
async fn pack_directory(
options : Options,
runtime : Runtime,
remote_source : Json,
) -> PackResult {
let cwd = @env.current_dir().unwrap_or(".")
let root = @platform.absolute(options.directory, cwd)
let destination = if options.stdout {
None
} else {
Some(
@platform.output_path(options.output, cwd) catch {
err => {
if @async.is_cancellation_error(err) {
raise err
}
raise ConfigurationError
}
},
)
}
let selection_options : @selection.SelectionOptions = {
include_patterns: options.include_patterns,
ignore: options.ignore,
use_gitignore: options.use_gitignore,
use_dot_ignore: options.use_dot_ignore,
use_default_patterns: options.use_default_patterns,
output: destination,
}
let selected = @selection.select(root, selection_options) catch {
err => {
if @async.is_cancellation_error(err) {
raise err
}
raise SelectionError
}
}
let accepted : Array[@document.File] = []
let files : Array[FileStats] = []
let transforms : Array[Json] = []
let excluded : Set[String] = Set([])
let skipped = selected.skipped.copy()
let findings : Array[Finding] = []
for candidate in selected.files {
@async.sleep(0)
match @input.read(candidate.path, options.max_file_size) {
Skipped(reason) => {
skipped.push({ path: candidate.relative, reason, })
excluded.add(context_path_key(candidate.relative))
}
Loaded(snapshot) => {
if options.security_check {
let hits = @security.scan(
candidate.relative,
snapshot.text,
snapshot.bom_bytes,
) catch {
_ => raise ScanError
}
if !hits.is_empty() {
findings.append(hits)
skipped.push({
path: candidate.relative,
reason: "SecurityFinding",
})
excluded.add(context_path_key(candidate.relative))
continue
}
}
if options.style is Xml &&
(
!@output.xml_representable(candidate.relative) ||
!@output.xml_representable(snapshot.text)
) {
skipped.push({
path: candidate.relative,
reason: "UnrepresentableText",
})
excluded.add(context_path_key(candidate.relative))
continue
}
let original_tokens = runtime.encoder.count(snapshot.text)
let mut text = snapshot.text
let mut tokens = original_tokens
if options.compress {
let result = @compression.compress(candidate.relative, snapshot.text)
let compressed_tokens = if result.applied {
runtime.encoder.count(result.text)
} else {
original_tokens
}
let applied = result.applied && compressed_tokens < original_tokens
if applied {
text = result.text
tokens = compressed_tokens
}
let ranges : Array[Json] = if applied {
context_ranges(result.ranges)
} else {
[]
}
let reason : Json = if result.applied && !applied {
Json("not-fewer-tokens")
} else {
match result.reason {
None => Json::null()
Some(reason) => Json(reason)
}
}
let language : Json = match result.language {
None => Json::null()
Some(name) => Json(name)
}
transforms.push({
"path": candidate.relative,
"language": language,
"applied": applied,
"reason": reason,
"ranges": ranges,
"originalTokens": original_tokens,
"outputTokens": tokens,
})
}
accepted.push({ path: candidate.relative, text, })
files.push({
path: candidate.relative,
disk_bytes: snapshot.disk_bytes,
tokens,
original_tokens,
})
}
}
}
let git = git_context(
selected.root,
options,
selection_options,
excluded,
skipped,
findings,
)
let metadata : Json = if options.compress ||
options.include_diffs ||
options.include_logs ||
remote_source != Json::null() {
let source : Json = if remote_source != Json::null() {
remote_source
} else {
let revision : Json = match git.revision {
None => Json::null()
Some(id) => Json(id)
}
{ "kind": "local", "revision": revision }
}
{ "source": source, "compression": transforms, "git": git.metadata }
} else {
Json::null()
}
if options.style is Xml &&
metadata != Json::null() &&
!@output.xml_representable(metadata.stringify()) {
raise ConfigurationError
}
let document = @document.create(accepted, sections=git.sections, metadata~)
let text = match options.style {
Xml => @output.xml(document, full_metadata=options.full_metadata)
Markdown => @output.markdown(document, full_metadata=options.full_metadata)
Json => @output.json(document, full_metadata=options.full_metadata)
Plain => @output.plain(document, full_metadata=options.full_metadata)
}
@async.sleep(0)
let tokens = runtime.encoder.count(text)
let budget = match options.token_budget {
None => NotSet
Some(limit) if tokens > limit => Exceeded(limit)
Some(limit) => Within(limit)
}
{
document,
text,
bytes: @utf8.encode(text).length(),
tokens,
files,
skipped,
findings,
security: if !options.security_check {
Disabled
} else if findings.is_empty() {
Passed
} else {
Excluded
},
budget,
}
}