F435FWSFVVX77ADD2R27X3TTECEYWAVV7X6MDLMMTGBJZBCRMMFQC <?phpuse Illuminate\Process\PendingProcess;use Illuminate\Support\Facades\Process;it('lists known intent tags', function () {Process::fake(function (PendingProcess $process) {return match ($process->command) {['pijul', 'log', '--limit', '500', '--output-format', 'json'] => Process::result(json_encode([['message' => 'feat(pijul-qol): first'],['message' => 'feat(init): second'],['message' => 'plain message'],['message' => 'feat(pijul-qol): duplicate'],], JSON_THROW_ON_ERROR)),default => Process::result('', 'Unexpected command.', 1),};});$this->artisan('pijul:record-with-intent-tags', ['--list-tags' => true])->expectsOutput('feat(pijul-qol)')->expectsOutput('feat(init)')->assertExitCode(0);Process::assertDidntRun(fn (PendingProcess $process) => is_array($process->command)&& ($process->command[1] ?? null) === 'record');});it('records a change with a reused intent tag', function () {Process::fake(function (PendingProcess $process) {return match ($process->command) {['pijul', 'log', '--limit', '500', '--output-format', 'json'] => Process::result(json_encode([['message' => 'feat(pijul-qol): first'],['message' => 'feat(init): second'],], JSON_THROW_ON_ERROR)),['pijul', 'record', '-m', 'feat(pijul-qol): add laravel command', 'app'] => Process::result(''),default => Process::result('', 'Unexpected command.', 1),};});$this->artisan('pijul:record-with-intent-tags', ['prefixes' => ['app']])->expectsOutputToContain('Known tags: feat(pijul-qol), feat(init)')->expectsQuestion('Intent tag (optional)', 'feat(pijul-qol)')->expectsQuestion('message', 'add laravel command')->assertExitCode(0);});it('forwards an explicit message without prompting', function () {Process::fake(function (PendingProcess $process) {return match ($process->command) {['pijul', 'log', '--limit', '500', '--output-format', 'json'] => Process::result(json_encode([['message' => 'feat(pijul-qol): first'],], JSON_THROW_ON_ERROR)),['pijul', 'record', '-m', 'ship it', '--all', '--description', 'desc', 'app'] => Process::result(''),default => Process::result('', 'Unexpected command.', 1),};});$this->artisan('pijul:record-with-intent-tags', ['--message' => 'ship it','--all' => true,'--description' => 'desc','prefixes' => ['app'],])->expectsOutputToContain('Known tags: feat(pijul-qol)')->assertExitCode(0);});
<?phpuse Illuminate\Process\PendingProcess;use Illuminate\Support\Facades\Process;it('shows change messages inline in pijul credit output', function () {Process::fake(function (PendingProcess $process) {return match ($process->command) {['pijul', 'credit', 'README.md'] => Process::result(<<<'OUTPUT'ABC123A regular lineOUTPUT),['pijul', 'change', 'ABC123'] => Process::result("message = \"feat(pijul-qol): inline context\"\n"),default => Process::result('', 'Unexpected command.', 1),};});$this->artisan('pijul:credit-with-messages', ['file' => 'README.md'])->expectsOutputToContain('ABC123 (feat(pijul-qol): inline context)')->expectsOutputToContain('A regular line')->assertExitCode(0);});
#!/usr/bin/env bashset -euo pipefailTAG_LIMIT="${PIJUL_RECORD_TAG_LIMIT:-500}"usage() {cat <<'EOF'Use previously seen intent tags while creating a new Pijul change.Usage:scripts/pijul-record-with-intent-tags [pijul record options and prefixes...]scripts/pijul-record-with-intent-tags --list-tagsBehavior:- Extracts recent intent tags from `pijul log`.- Uses `fzf` for interactive selection when available.- Otherwise prints known tags as a reminder and asks for the message body.- If `-m/--message` is already provided, it prints known tags and forwardsdirectly to `pijul record`.Environment:PIJUL_RECORD_TAG_LIMIT Number of recent log entries to scan (default: 500)EOF}has_message_arg() {local argfor arg in "$@"; docase "$arg" in--)return 1;;-m|--message|--message=*|-m?*)return 0;;esacdonereturn 1}collect_intent_tags() {pijul log --limit "$TAG_LIMIT" 2>/dev/null \| sed -nE 's/^ ([[:alnum:]_-]+\([^)]*\)).*/\1/p' \| awk 'NF && !seen[$0]++'}join_lines() {awk 'NR == 1 { printf "%s", $0; next }{ printf ", %s", $0 }END { if (NR > 0) printf "\n" }'}print_known_tags() {local tags="$1"if [[ -z "$tags" ]]; thenreturnfiprintf 'Known tags: %s\n' "$(printf '%s\n' "$tags" | join_lines)" >&2}select_tag() {local tags="$1"if [[ -z "$tags" || ! -t 0 ]]; thenreturnfiif command -v fzf >/dev/null 2>&1; thenprintf '%s\n' "$tags" \| fzf --prompt='intent tag (Esc=skip)> ' --height=40% --reverse \|| truereturnfiprint_known_tags "$tags"}main() {local tags=""local tag=""local body=""local message=""case "${1-}" in--help|-h)usageexit 0;;--list-tags)collect_intent_tags || trueexit 0;;esactags="$(collect_intent_tags || true)"if has_message_arg "$@"; thenif [[ -t 1 ]]; thenprint_known_tags "$tags"fiexec pijul record "$@"fiif [[ ! -t 0 ]]; thenprintf 'This helper needs a terminal unless you pass -m/--message.\n' >&2exit 1fitag="$(select_tag "$tags")"tag="${tag%%$'\n'*}"tag="${tag%$'\r'}"if [[ -n "$tag" ]]; thenprintf 'Using tag: %s\n' "$tag" >&2firead -erp "message: " body < /dev/ttyif [[ -n "$tag" && -n "$body" ]]; thenmessage="$tag: $body"elif [[ -n "$tag" ]]; thenmessage="$tag"elsemessage="$body"fiif [[ -z "$message" ]]; thenprintf 'Change message cannot be empty.\n' >&2exit 1fiexec pijul record -m "$message" "$@"}main "$@"
#!/usr/bin/env bashset -euo pipefaildeclare -A CHANGE_MESSAGESresolve_message() {local hash="$1"if [[ -n "${CHANGE_MESSAGES[$hash]+x}" ]]; thenprintf '%s' "${CHANGE_MESSAGES[$hash]}"returnfilocal raw_messageraw_message="$(pijul change "$hash" 2>/dev/null | sed -nE 's/^message = "(.*)"$/\1/p' | head -n 1 || true)"if [[ -z "$raw_message" ]]; thenCHANGE_MESSAGES["$hash"]="<message unavailable>"elseCHANGE_MESSAGES["$hash"]="${raw_message//\\\"/\"}"fiprintf '%s' "${CHANGE_MESSAGES[$hash]}"}is_hash_line() {local line="$1"[[ "$line" =~ ^[A-Z0-9]+([[:space:]]*,[[:space:]]*[A-Z0-9]+)*$ ]]}format_hash_line() {local line="$1"local formatted=""local index=0local hashlocal messageIFS=',' read -ra hashes <<< "$line"for hash in "${hashes[@]}"; dohash="${hash#"${hash%%[![:space:]]*}"}"hash="${hash%"${hash##*[![:space:]]}"}"message="$(resolve_message "$hash")"if (( index > 0 )); thenformatted+=", "fiformatted+="${hash} (${message})"((index += 1))doneprintf '%s\n' "$formatted"}while IFS= read -r line || [[ -n "$line" ]]; doif is_hash_line "$line"; thenformat_hash_line "$line"elseprintf '%s\n' "$line"fidone < <(pijul credit "$@")
<?phpnamespace App\Commands;use App\Commands\Concerns\InteractsWithPijul;use JsonException;use LaravelZero\Framework\Commands\Command;class PijulRecordWithIntentTagsCommand extends Command{use InteractsWithPijul;/*** @var string*/protected $signature = 'pijul:record-with-intent-tags{prefixes?* : Paths in which to record the changes}{--a|all : Record all paths that have changed}{--m|message= : Set the change message}{--description= : Set the description field}{--author= : Set the author field}{--timestamp= : Set the timestamp field}{--ignore-missing : Ignore missing (deleted) files}{--working-copy= : Override the working copy path}{--amend= : Amend this change instead of creating a new change}{--identity= : Identity to sign changes with}{--patience : Use Patience diff}{--histogram : Use Histogram diff}{--repository= : Work with the repository at PATH}{--channel= : Work with CHANNEL instead of the current channel}{--no-prompt : Abort rather than prompt for input}{--list-tags : Print known intent tags and exit}{--tag-limit=500 : Number of recent log entries to scan}';/*** @var string*/protected $description = 'Record a Pijul change with reusable intent tags';/*** @var array<int, string>*/protected $aliases = ['pijul-record-with-intent-tags'];/*** @throws JsonException*/public function handle(): int{$tags = $this->collectIntentTags();if ($this->option('list-tags')) {foreach ($tags as $tag) {$this->line($tag);}return self::SUCCESS;}$message = $this->option('message');if ($message !== null) {$this->printKnownTags($tags);$this->runPijul($this->recordArguments((string) $message), interactive: $this->input->isInteractive());return self::SUCCESS;}if (! $this->input->isInteractive()) {$this->error('This command needs an interactive terminal unless you pass --message.');return self::FAILURE;}$this->printKnownTags($tags);$tag = $tags === []? '': (string) $this->anticipate('Intent tag (optional)', $tags, '');$body = (string) $this->ask('message');$fullMessage = $this->buildMessage($tag, $body);if ($fullMessage === '') {$this->error('Change message cannot be empty.');return self::FAILURE;}$this->runPijul($this->recordArguments($fullMessage), interactive: true);return self::SUCCESS;}/*** @return list<string>** @throws JsonException*/private function collectIntentTags(): array{$arguments = ['log','--limit',(string) $this->option('tag-limit'),'--output-format','json',];foreach (['repository', 'channel'] as $option) {$value = $this->option($option);if ($value !== null) {$arguments[] = sprintf('--%s', $option);$arguments[] = (string) $value;}}if ($this->option('no-prompt')) {$arguments[] = '--no-prompt';}$result = $this->runPijul($arguments);$changes = json_decode($result->output(), true, 512, JSON_THROW_ON_ERROR);$tags = [];foreach ($changes as $change) {$message = $change['message'] ?? null;if (! is_string($message) || preg_match('/^([A-Za-z0-9_-]+\([^)]*\))/', $message, $matches) !== 1) {continue;}$tags[$matches[1]] = true;}return array_keys($tags);}private function printKnownTags(array $tags): void{if ($tags === []) {return;}$this->line(sprintf('Known tags: %s', implode(', ', $tags)));}private function buildMessage(string $tag, string $body): string{$tag = trim($tag);$body = trim($body);if ($tag !== '' && $body !== '') {return sprintf('%s: %s', $tag, $body);}return $tag !== '' ? $tag : $body;}/*** @return array<int, string>*/private function recordArguments(string $message): array{$arguments = ['record', '-m', $message];if ($this->option('all')) {$arguments[] = '--all';}foreach (['description','author','timestamp','working-copy','identity','repository','channel',] as $option) {$value = $this->option($option);if ($value !== null) {$arguments[] = sprintf('--%s', $option);$arguments[] = (string) $value;}}if ($this->input->hasParameterOption('--amend')) {$arguments[] = '--amend';if (($amend = $this->option('amend')) !== null) {$arguments[] = (string) $amend;}}foreach (['ignore-missing', 'patience', 'histogram', 'no-prompt'] as $option) {if ($this->option($option)) {$arguments[] = sprintf('--%s', $option);}}foreach ($this->argument('prefixes') as $prefix) {$arguments[] = (string) $prefix;}return $arguments;}}
<?phpnamespace App\Commands;use App\Commands\Concerns\InteractsWithPijul;use LaravelZero\Framework\Commands\Command;class PijulCreditWithMessagesCommand extends Command{use InteractsWithPijul;/*** @var string*/protected $signature = 'pijul:credit-with-messages{file : The file to annotate}{--repository= : Work with the repository at PATH}{--channel= : Work with CHANNEL instead of the current channel}{--no-prompt : Abort rather than prompt for input}';/*** @var string*/protected $description = 'Show pijul credit output with change messages inline';/*** @var array<int, string>*/protected $aliases = ['pijul-credit-with-messages'];/*** @var array<string, string>*/private array $changeMessages = [];public function handle(): int{$output = $this->runPijul($this->creditArguments())->output();foreach (preg_split("/\r\n|\n|\r/", rtrim($output, "\r\n")) ?: [] as $line) {$this->line($this->isHashLine($line) ? $this->formatHashLine($line) : $line);}if (str_ends_with($output, "\n\n")) {$this->newLine();}return self::SUCCESS;}/*** @return array<int, string>*/private function creditArguments(): array{$arguments = ['credit'];if (($repository = $this->option('repository')) !== null) {$arguments[] = '--repository';$arguments[] = $repository;}if (($channel = $this->option('channel')) !== null) {$arguments[] = '--channel';$arguments[] = $channel;}if ($this->option('no-prompt')) {$arguments[] = '--no-prompt';}$arguments[] = (string) $this->argument('file');return $arguments;}private function isHashLine(string $line): bool{return preg_match('/^[A-Z0-9]+(?:\s*,\s*[A-Z0-9]+)*$/', $line) === 1;}private function formatHashLine(string $line): string{$hashes = preg_split('/\s*,\s*/', trim($line)) ?: [];$formatted = [];foreach ($hashes as $hash) {$formatted[] = sprintf('%s (%s)', $hash, $this->resolveMessage($hash));}return implode(', ', $formatted);}private function resolveMessage(string $hash): string{if (array_key_exists($hash, $this->changeMessages)) {return $this->changeMessages[$hash];}$changeArguments = ['change'];if (($repository = $this->option('repository')) !== null) {$changeArguments[] = '--repository';$changeArguments[] = $repository;}if ($this->option('no-prompt')) {$changeArguments[] = '--no-prompt';}$changeArguments[] = $hash;$rawOutput = $this->runPijul($changeArguments)->output();if (preg_match('/^message = "(.*)"$/m', $rawOutput, $matches) !== 1) {return $this->changeMessages[$hash] = '<message unavailable>';}return $this->changeMessages[$hash] = str_replace('\"', '"', $matches[1]);}}
<?phpnamespace App\Commands\Concerns;use Illuminate\Contracts\Process\ProcessResult;use Illuminate\Process\PendingProcess;use Illuminate\Support\Facades\Process;use RuntimeException;trait InteractsWithPijul{protected function runPijul(array $arguments, bool $interactive = false): ProcessResult{$pendingProcess = Process::path(base_path());if ($interactive) {$pendingProcess = $this->configureInteractiveProcess($pendingProcess);}$result = $pendingProcess->run(array_merge(['pijul'], $arguments));if ($result->failed()) {$errorOutput = trim($result->errorOutput());throw new RuntimeException($errorOutput !== ''? $errorOutput: sprintf('The command "%s" failed.', $result->command()));}return $result;}private function configureInteractiveProcess(PendingProcess $pendingProcess): PendingProcess{$pendingProcess = $pendingProcess->forever();if (Process::supportsTty()) {$pendingProcess = $pendingProcess->tty();}return $pendingProcess;}}
Use `scripts/pijul-credit-with-messages` to keep `pijul credit` output intact while showing each hash's change message inline.
Use `./pijultester pijul:credit-with-messages` to keep `pijul credit` output intact while showing each hash's change message inline.
Use `scripts/pijul-record-with-intent-tags` to reuse intent tags from recent `pijul log` messages while recording a change.
Use `./pijultester pijul:record-with-intent-tags` to reuse intent tags from recent `pijul log` messages while recording a change.
- If `fzf` is installed, you can fuzzy-search existing tags and press Enter to reuse one.- If `fzf` is not installed, the script prints known tags as a reminder before asking for the rest of your message.- Press Esc in `fzf`, or just ignore the reminder, if you want to type a fresh message instead.
- The command lists known tags as a reminder before prompting.- The intent-tag prompt supports terminal autocompletion from recent `pijul log` messages.- Skip the tag prompt or pass `--message` if you want to type a fresh message directly.
scripts/pijul-record-with-intent-tagsscripts/pijul-record-with-intent-tags --allscripts/pijul-record-with-intent-tags --list-tags
./pijultester pijul:record-with-intent-tags./pijultester pijul:record-with-intent-tags --all./pijultester pijul:record-with-intent-tags --list-tags