Generated banner for Pr Reference
Generated logo for Pr Reference

Pr Reference

MicrosoftOfficialPublished by Microsoft

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Generates PR reference XML with commit history and unified diffs between branches, with extension and path filtering. Use when creating pull request descriptions, preparing code reviews, analyzing branch changes, discovering work items from diffs, or generating structured diff summaries.
— the description by Microsoft

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The pr-reference skill provides a set of scripts to extract git commit metadata and diffs into a structured XML format. It supports cross-platform execution via Bash and PowerShell, allowing developers to generate comprehensive branch comparisons. Beyond generation, the skill includes utility scripts to list changed files and inspect specific diff chunks, making it a versatile tool for automating pull request descriptions, conducting code reviews, and performing security analysis on code changes.

What it does

  • Generates an XML document containing commit history and unified diffs between branches.
  • Supports filtering diffs by file extensions and directory paths to reduce noise.
  • Provides utility scripts to list changed files with type-based filtering.
  • Enables inspection of large diffs through chunked reading capabilities.
  • Offers flexible base branch selection, including automatic remote default detection.
  • Supports three-way comparison using git merge-base.

When to use it

  • When you need to generate detailed pull request descriptions based on commit history.
  • When preparing for code reviews and requiring structured context of changes.
  • When you need to analyze branch changes or discover work items from diffs.
  • When you need to perform security audits on modified files.
  • When you need to exclude specific file types or paths from your diff analysis.

Advantages

  • Structured XML output facilitates programmatic processing and integration.
  • Cross-platform support ensures consistency across different development environments.
  • Granular filtering options help focus on relevant code changes.
  • Utility scripts simplify the extraction of specific file lists or diff segments.
  • Supports standard git workflows like three-way merges.

Usage examples

“Generate a PR reference comparing my current branch to the main branch.”
The skill executes the generation script, creating an XML file at .copilot-tracking/pr/pr-reference.xml containing the commit history and diffs.
“List all added and modified files from the generated PR reference.”
The skill runs the list-changed-files script, outputting a filtered list of files based on the specified change types.
“Read the diff for src/main.ts from the generated reference.”
The skill extracts and displays the specific unified diff content for the requested file.
“Generate a PR reference excluding all JSON and YAML files.”
The skill generates the XML report while applying the specified extension filters to the diff output.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/microsoft/pr-reference/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip pr-reference.zip -d .agents/skills

Documentation by Microsoft

PR Reference Generation Skill

Overview

Queries git for commit metadata and diff output, then produces a structured XML document. Both bash and PowerShell implementations are provided.

Use cases:

  • PR description generation from commit history

  • Code review preparation with structured diff context

  • Work item discovery by analyzing branch changes

  • Security analysis of modified files

After successful generation, include a file link to the absolute path of the XML output in the response.

Prerequisites

The repository must have at least one commit diverging from the base branch.

PlatformRuntime
macOS / LinuxBash (pre-installed)
WindowsPowerShell 7+ (pwsh)
Cross-platformPowerShell 7+ (pwsh)

Quick Start

Run the following command to generate a PR reference with default settings (compares against origin/main):

scripts/generate.sh
scripts/generate.sh --base-branch auto --merge-base --exclude-ext yml,json
scripts/generate.ps1
scripts/generate.ps1 -BaseBranch auto -MergeBase -ExcludeExt yml,json

Output saves to .copilot-tracking/pr/pr-reference.xml by default.

Parameters Reference

ParameterFlag (bash)Flag (PowerShell)DefaultDescription
Base branch--base-branch-BaseBranchorigin/main (bash) / main (PowerShell)Target branch for comparison. Use auto to detect remote default.
Merge base--merge-base-MergeBasefalseUse git merge-base for three-way comparison
Exclude markdown--no-md-diff-ExcludeMarkdownDifffalseExclude markdown files (*.md) from the diff
Exclude extensions--exclude-ext-ExcludeExt(none)Comma-separated extensions to exclude (e.g., yml,yaml,json,png)
Exclude paths--exclude-path-ExcludePath(none)Comma-separated path prefixes to exclude (for example, documentation and GitHub configuration directories)
Output path--output-OutputPath.copilot-tracking/pr/pr-reference.xmlCustom output file path

Both defaults resolve to the same remote comparison. The PowerShell script automatically resolves origin/<branch> when a bare branch name is provided.

Additional Scripts Reference

After generating the PR reference, use these utility scripts to query the XML.

List Changed Files

Run the list script to extract file paths from the diff:

scripts/list-changed-files.sh                              # all changed files
scripts/list-changed-files.sh --type added                  # filter by single type
scripts/list-changed-files.sh --type added,modified,renamed  # filter by multiple types
scripts/list-changed-files.sh --exclude-type deleted         # exclude specific types
scripts/list-changed-files.sh --format markdown              # output as markdown table
scripts/list-changed-files.ps1                             # all changed files
scripts/list-changed-files.ps1 -Type Added                  # filter by single type
scripts/list-changed-files.ps1 -Type Added,Modified,Renamed # filter by multiple types
scripts/list-changed-files.ps1 -ExcludeType Deleted          # exclude specific types
scripts/list-changed-files.ps1 -Format Json                  # output as JSON
ParameterFlag (bash)Flag (PowerShell)DefaultDescription
Input path--input, -i-InputPath(auto)Path to pr-reference.xml
Type filter--type, -t-TypeallChange types to include (comma-separated: added, modified, etc.)
Exclude type--exclude-type-ExcludeType(none)Change types to exclude. Mutually exclusive with --type (non-all)
Format--format, -f-FormatplainOutput format: plain, json, or markdown
Read Diff Content

Run the read script to inspect diff content with chunking for large diffs:

scripts/read-diff.sh --info             # chunk info (count, line ranges)
scripts/read-diff.sh --chunk 1          # read a specific chunk
scripts/read-diff.sh --file src/main.ts  # extract diff for one file
scripts/read-diff.sh --summary          # file stats summary
scripts/read-diff.ps1 -Info              # chunk info
scripts/read-diff.ps1 -Chunk 1           # read a specific chunk
scripts/read-diff.ps1 -File "src/main.ts" # extract diff for one file

Output Format

The generated XML wraps commit metadata and unified diff output in a <commit_history> root element. See the reference guide for the complete XML schema, element reference, output path variations, and workflow integration patterns.

Troubleshooting

SymptomCauseResolution
"No commits found" or empty XMLNo diverging commits from base branchVerify the branch has commits ahead of the base with git log base..HEAD
"Branch not found" errorBase branch ref missing locallyRun git fetch origin to update remote tracking refs
"git: command not found"git is not on PATHInstall git or verify PATH includes the git binary directory

Files

tests/list-changed-files.Tests.ps1

#Requires -Modules Pester
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT

BeforeAll {
    . (Join-Path -Path $PSScriptRoot -ChildPath '../scripts/list-changed-files.ps1')

    # Fixture XML with all four change types
    $script:FixtureXml = @'
<commit_history>
  <current_branch>feature/test</current_branch>
  <base_branch>origin/main</base_branch>
  <commits>
    <commit hash="abc1234" date="2026-01-15">
      <message>
        <subject><![CDATA[test commit]]></subject>
        <body><![CDATA[]]></body>
      </message>
    </commit>
  </commits>
  <full_diff>
diff --git a/src/new-file.ts b/src/new-file.ts
new file mode 100644
index 0000000..a1b2c3d
--- /dev/null
+++ b/src/new-file.ts
@@ -0,0 +1,3 @@
+export function newFeature() {
+  return true;
+}

diff --git a/src/existing.ts b/src/existing.ts
index a1b2c3d..d4e5f6a 100644
--- a/src/existing.ts
+++ b/src/existing.ts
@@ -1,3 +1,4 @@
 export function existing() {
-  return false;
+  return true;
+  // comment
 }

diff --git a/src/removed.ts b/src/removed.ts
deleted file mode 100644
index a1b2c3d..0000000
--- a/src/removed.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function removed() {
-  return null;
-}

diff --git a/src/old-name.ts b/src/new-name.ts
rename from src/old-name.ts
rename to src/new-name.ts
index a1b2c3d..d4e5f6a 100644
--- a/src/old-name.ts
+++ b/src/new-name.ts
@@ -1,3 +1,3 @@
 export function renamed() {
-  return 'old';
+  return 'new';
 }
  </full_diff>
</commit_history>
'@
    $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "lcf-tests-$([System.Guid]::NewGuid())"
    New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null
    $script:FixturePath = Join-Path $script:TempDir 'pr-reference.xml'
    Set-Content -Path $script:FixturePath -Value $script:FixtureXml -NoNewline
}

AfterAll {
    Remove-Item -Path $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue
}

Describe 'Get-FileChanges' -Tag 'Unit' {
    Context 'All change types' {
        It 'Returns all four changed files when filtering by All' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType 'All'
            $changes.Count | Should -Be 4
        }

        It 'Returns results sorted by Path' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType 'All'
            $paths = @($changes | Select-Object -ExpandProperty Path)
            $sorted = @($paths | Sort-Object)
            $paths | Should -Be $sorted
        }

        It 'Classifies each change type correctly' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType 'All'
            ($changes | Where-Object Type -eq 'Added').Count | Should -Be 1
            ($changes | Where-Object Type -eq 'Modified').Count | Should -Be 1
            ($changes | Where-Object Type -eq 'Deleted').Count | Should -Be 1
            ($changes | Where-Object Type -eq 'Renamed').Count | Should -Be 1
        }
    }

    Context 'Added files' {
        It 'Returns only new file mode files' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType 'Added'
            $changes.Count | Should -Be 1
            $changes[0].Type | Should -Be 'Added'
            $changes[0].Path | Should -Be 'src/new-file.ts'
        }
    }

    Context 'Deleted files' {
        It 'Returns only deleted file mode files' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType 'Deleted'
            $changes.Count | Should -Be 1
            $changes[0].Type | Should -Be 'Deleted'
            $changes[0].Path | Should -Be 'src/removed.ts'
        }
    }

    Context 'Modified files' {
        It 'Returns only modified files (no mode header)' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType 'Modified'
            $changes.Count | Should -Be 1
            $changes[0].Type | Should -Be 'Modified'
            $changes[0].Path | Should -Be 'src/existing.ts'
        }
    }

    Context 'Renamed files' {
        It 'Returns only renamed files' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType 'Renamed'
            $changes.Count | Should -Be 1
            $changes[0].Type | Should -Be 'Renamed'
        }

        It 'Uses arrow notation for old -> new path' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType 'Renamed'
            $changes[0].Path | Should -Be 'src/old-name.ts -> src/new-name.ts'
        }
    }

    Context 'Rename by path mismatch' {
        BeforeAll {
            $xml = @'
<commit_history>
  <full_diff>
diff --git a/old/path.ts b/new/path.ts
index a1b2c3d..d4e5f6a 100644
--- a/old/path.ts
+++ b/new/path.ts
@@ -1 +1 @@
-old content
+new content
  </full_diff>
</commit_history>
'@
            $script:RenameFixture = Join-Path $script:TempDir 'rename-path.xml'
            Set-Content -Path $script:RenameFixture -Value $xml -NoNewline
        }

        It 'Detects rename when a/ and b/ paths differ without explicit rename header' {
            $changes = Get-FileChanges -XmlPath $script:RenameFixture -FilterType 'All'
            $changes[0].Type | Should -Be 'Renamed'
            $changes[0].Path | Should -Be 'old/path.ts -> new/path.ts'
        }
    }

    Context 'Header-like text inside diff content' {
        BeforeAll {
            $diffLines = @'
diff --git a/docs/headers.md b/docs/headers.md
index 1111111..2222222 100644
--- a/docs/headers.md
+++ b/docs/headers.md
@@ -1,3 +1,3 @@
 diff --git a/path/to/file b/path/to/file
-old line
+diff --git a/x b/x
  diff --git a/edge b/edge
diff --git a/docs/added-note.md b/docs/added-note.md
index 3333333..4444444 100644
--- a/docs/added-note.md
+++ b/docs/added-note.md
@@ -1,2 +1,2 @@
 new file mode 100644
-old line
+new line
diff --git a/docs/deleted-note.md b/docs/deleted-note.md
index 5555555..6666666 100644
--- a/docs/deleted-note.md
+++ b/docs/deleted-note.md
@@ -1 +1 @@
-old line
+deleted file mode 100644
diff --git a/docs/renamed-note.md b/docs/renamed-note.md
index 7777777..8888888 100644
--- a/docs/renamed-note.md
+++ b/docs/renamed-note.md
@@ -1,2 +1,2 @@
 rename from docs/old-note.md
-old line
+new line
diff --git a/src/gone.ts b/src/gone.ts
deleted file mode 100644
index 9999999..0000000
--- a/src/gone.ts
+++ /dev/null
@@ -1 +0,0 @@
-// new file mode 100644
diff --git a/src/fresh.ts b/src/fresh.ts
new file mode 100644
index 0000000..1212121
--- /dev/null
+++ b/src/fresh.ts
@@ -0,0 +1 @@
+// deleted file mode 100644
diff --git a/src/old-name.ts b/src/new-name.ts
similarity index 90%
rename from src/old-name.ts
rename to src/new-name.ts
index 1313131..1414141 100644
--- a/src/old-name.ts
+++ b/src/new-name.ts
@@ -1 +1 @@
-old line
+new line
'@ -split '\r?\n'

            # generate.sh writes the diff unindented; generate.ps1 indents each line by two spaces
            $writeFixture = {
                param([string]$Name, [string[]]$Lines, [string]$NewLine)
                $path = Join-Path $TestDrive $Name
                $xml = @('<commit_history>', '  <full_diff>') + $Lines + @('  </full_diff>', '</commit_history>')
                Set-Content -Path $path -Value ($xml -join $NewLine) -NoNewline
                return $path
            }
            $script:HeaderTextFixtures = @{
                Unindented = & $writeFixture 'header-text.xml' $diffLines "`n"
                Indented   = & $writeFixture 'header-text-indented.xml' ($diffLines | ForEach-Object { "  $_" }) "`r`n"
            }
        }

        It 'Lists only files with a real diff header, typed from header lines (<Fixture>)' -ForEach @(
            @{ Fixture = 'Unindented' }
            @{ Fixture = 'Indented' }
        ) {
            $changes = Get-FileChanges -XmlPath $script:HeaderTextFixtures[$Fixture]
            @($changes | ForEach-Object { "$($_.Path)=$($_.Type)" }) | Should -Be @(
                'docs/added-note.md=Modified'
                'docs/deleted-note.md=Modified'
                'docs/headers.md=Modified'
                'docs/renamed-note.md=Modified'
                'src/fresh.ts=Added'
                'src/gone.ts=Deleted'
                'src/old-name.ts -> src/new-name.ts=Renamed'
            )
        }

        It 'Keeps modified files when excluding deleted files (<Fixture>)' -ForEach @(
            @{ Fixture = 'Unindented' }
            @{ Fixture = 'Indented' }
        ) {
            $changes = Get-FileChanges -XmlPath $script:HeaderTextFixtures[$Fixture] -ExcludeFilterType @('Deleted')
            @($changes.Path) | Should -Be @(
                'docs/added-note.md'
                'docs/deleted-note.md'
                'docs/headers.md'
                'docs/renamed-note.md'
                'src/fresh.ts'
                'src/old-name.ts -> src/new-name.ts'
            )
        }
    }

    Context 'Header-like text inside commit messages' {
        BeforeAll {
            $phantom = @(
                'diff --git a/phantom.txt b/phantom.txt'
                'new file mode 100644'
            )
            $diffLines = @(
                'diff --git a/src/real.ts b/src/real.ts'
                'index 1111111..2222222 100644'
                '--- a/src/real.ts'
                '+++ b/src/real.ts'
                '@@ -1 +1 @@'
                '-old line'
                '+new line'
            )

            # generate.sh writes commits and diff at column 0; generate.ps1 indents both by two spaces.
            # git log writes no newline after the last entry, so generate.sh closes </commits> on that line.
            $writeFixture = {
                param([string]$Name, [string]$Prefix, [string]$NewLine, [string[]]$BodyLines, [bool]$JoinClose = $false)
                $path = Join-Path $TestDrive $Name
                $commitLines = @('<commit hash="abc1234" date="2026-01-15"><message><subject><![CDATA[fix: probe]]></subject><body><![CDATA[Body line') +
                    $BodyLines +
                    @(']]></body></message></commit>')
                $entries = @($commitLines | ForEach-Object { "$Prefix$_" })
                $closeLines = @('  </commits>', '', '  <full_diff>')
                if ($JoinClose) {
                    $entries[-1] += $closeLines[0]
                    $closeLines = $closeLines[1..2]
                }
                $xml = @('<commit_history>', '  <commits>') + $entries + $closeLines +
                    ($diffLines | ForEach-Object { "$Prefix$_" }) +
                    @('  </full_diff>', '</commit_history>')
                Set-Content -Path $path -Value ($xml -join $NewLine) -NoNewline
                return $path
            }

            # Bodies are not escaped, so one can also repeat the markers that frame the diff block
            $bareMarkers = @('</commits>', '<full_diff>')
            $generatorMarkers = @('  </commits>', '', '  <full_diff>')
            $script:CommitTextFixtures = @{
                Unindented             = & $writeFixture 'commit-text.xml' '' "`n" $phantom
                Indented               = & $writeFixture 'commit-text-indented.xml' '  ' "`r`n" $phantom
                ClosedOnLastCommitLine = & $writeFixture 'commit-text-joined.xml' '' "`n" $phantom $true
                BareMarkers            = & $writeFixture 'commit-text-bare.xml' '' "`n" ($bareMarkers + $phantom)
                BareMarkersIndented    = & $writeFixture 'commit-text-bare-indented.xml' '  ' "`r`n" ($bareMarkers + $phantom)
                GeneratorMarkers       = & $writeFixture 'commit-text-markers.xml' '' "`n" ($generatorMarkers + $phantom)
            }
        }

        It 'Ignores diff headers written in commit bodies (<Fixture>)' -ForEach @(
            @{ Fixture = 'Unindented' }
            @{ Fixture = 'Indented' }
            @{ Fixture = 'ClosedOnLastCommitLine' }
            @{ Fixture = 'BareMarkers' }
            @{ Fixture = 'BareMarkersIndented' }
            @{ Fixture = 'GeneratorMarkers' }
        ) {
            $changes = Get-FileChanges -XmlPath $script:CommitTextFixtures[$Fixture]
            @($changes | ForEach-Object { "$($_.Path)=$($_.Type)" }) | Should -Be @('src/real.ts=Modified')
        }
    }

    Context 'Full-diff opening tags inside hunk context' {
        It 'Preserves files before a context tag (<Fixture>)' -ForEach @(
            @{ Fixture = 'unindented-lf-middle'; Prefix = ''; NewLine = "`n"; AtEnd = $false; IncludeCommits = $true }
            @{ Fixture = 'unindented-lf-end'; Prefix = ''; NewLine = "`n"; AtEnd = $true; IncludeCommits = $true }
            @{ Fixture = 'unindented-crlf-middle'; Prefix = ''; NewLine = "`r`n"; AtEnd = $false; IncludeCommits = $true }
            @{ Fixture = 'unindented-crlf-end'; Prefix = ''; NewLine = "`r`n"; AtEnd = $true; IncludeCommits = $true }
            @{ Fixture = 'indented-lf-middle'; Prefix = '  '; NewLine = "`n"; AtEnd = $false; IncludeCommits = $true }
            @{ Fixture = 'indented-lf-end'; Prefix = '  '; NewLine = "`n"; AtEnd = $true; IncludeCommits = $true }
            @{ Fixture = 'indented-crlf-middle'; Prefix = '  '; NewLine = "`r`n"; AtEnd = $false; IncludeCommits = $true }
            @{ Fixture = 'indented-crlf-end'; Prefix = '  '; NewLine = "`r`n"; AtEnd = $true; IncludeCommits = $true }
            @{ Fixture = 'no-commits-lf-middle'; Prefix = ''; NewLine = "`n"; AtEnd = $false; IncludeCommits = $false }
            @{ Fixture = 'no-commits-lf-end'; Prefix = ''; NewLine = "`n"; AtEnd = $true; IncludeCommits = $false }
            @{ Fixture = 'no-commits-crlf-middle'; Prefix = ''; NewLine = "`r`n"; AtEnd = $false; IncludeCommits = $false }
            @{ Fixture = 'no-commits-crlf-end'; Prefix = ''; NewLine = "`r`n"; AtEnd = $true; IncludeCommits = $false }
        ) {
            $diffLines = @(
                'diff --git a/docs/a.md b/docs/a.md'
                'index 1111111..2222222 100644'
                '--- a/docs/a.md'
                '+++ b/docs/a.md'
                '@@ -1,3 +1,3 @@'
            )
            $diffLines += if ($AtEnd) {
                @(' before', '-old line', '+new line', '  <full_diff>')
            } else {
                @('  <full_diff>', '-old line', '+new line', ' after')
            }
            $diffLines += @(
                'diff --git a/src/after.ts b/src/after.ts'
                'new file mode 100644'
                'index 0000000..3333333'
                '--- /dev/null'
                '+++ b/src/after.ts'
                '@@ -0,0 +1 @@'
                '+new content'
            )

            $xml = @('<commit_history>')
            if ($IncludeCommits) {
                $xml += '  <commits>'
                $entry = "${Prefix}" + '<commit hash="abc1234" date="2026-01-15"><message><subject><![CDATA[update]]></subject><body><![CDATA[]]></body></message></commit>'
                # The Bash generator appends the closing tag to the final commit line.
                $xml += if ($Prefix -eq '') { @("$entry  </commits>") } else { @($entry, '  </commits>') }
            }
            $xml += @('', '  <full_diff>') +
                @($diffLines | ForEach-Object { "$Prefix$_" }) +
                @('  </full_diff>', '</commit_history>')
            $path = Join-Path $TestDrive "$Fixture.xml"
            Set-Content -LiteralPath $path -Value ($xml -join $NewLine) -NoNewline

            $changes = Get-FileChanges -XmlPath $path
            @($changes | ForEach-Object { "$($_.Path)=$($_.Type)" }) | Should -Be @(
                'docs/a.md=Modified'
                'src/after.ts=Added'
            )
        }
    }

    Context 'Empty diff' {
        BeforeAll {
            $xml = @'
<commit_history>
  <full_diff>
  </full_diff>
</commit_history>
'@
            $script:EmptyFixture = Join-Path $script:TempDir 'empty-diff.xml'
            Set-Content -Path $script:EmptyFixture -Value $xml -NoNewline
        }

        It 'Returns empty result when no diff headers present' {
            $changes = Get-FileChanges -XmlPath $script:EmptyFixture -FilterType 'All'
            $changes | Should -BeNullOrEmpty
        }
    }

    Context 'Default FilterType' {
        It 'Returns all files when FilterType is omitted' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath
            $changes.Count | Should -Be 4
        }
    }

    Context 'Comma-separated Type values' {
        It 'Filters by multiple types as array' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType @('Added', 'Modified')
            $changes.Count | Should -Be 2
            ($changes | Where-Object Type -eq 'Added').Count | Should -Be 1
            ($changes | Where-Object Type -eq 'Modified').Count | Should -Be 1
        }

        It 'Filters by comma-separated string' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType @('Added,Renamed')
            $changes.Count | Should -Be 2
            ($changes | Where-Object Type -eq 'Added').Count | Should -Be 1
            ($changes | Where-Object Type -eq 'Renamed').Count | Should -Be 1
        }

        It 'Returns single type from multi-type filter' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -FilterType @('Deleted')
            $changes.Count | Should -Be 1
            $changes[0].Type | Should -Be 'Deleted'
        }
    }

    Context 'ExcludeFilterType parameter' {
        It 'Excludes deleted files' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -ExcludeFilterType @('Deleted')
            $changes.Count | Should -Be 3
            ($changes | Where-Object Type -eq 'Deleted').Count | Should -Be 0
        }

        It 'Excludes multiple types' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -ExcludeFilterType @('Deleted', 'Renamed')
            $changes.Count | Should -Be 2
            ($changes | Where-Object Type -eq 'Deleted').Count | Should -Be 0
            ($changes | Where-Object Type -eq 'Renamed').Count | Should -Be 0
        }

        It 'Returns all files when exclude list is empty' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -ExcludeFilterType @()
            $changes.Count | Should -Be 4
        }

        It 'Returns empty result when all types are excluded' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -ExcludeFilterType @('Added', 'Modified', 'Deleted', 'Renamed')
            $changes | Should -BeNullOrEmpty
        }

        It 'Accepts comma-separated exclude string' {
            $changes = Get-FileChanges -XmlPath $script:FixturePath -ExcludeFilterType @('Deleted,Renamed')
            $changes.Count | Should -Be 2
        }
    }

    Context 'Invalid type values' {
        It 'Throws for invalid filter type' {
            { Get-FileChanges -XmlPath $script:FixturePath -FilterType @('Invalid') } | Should -Throw '*Invalid type filter*'
        }

        It 'Throws for invalid exclude type' {
            { Get-FileChanges -XmlPath $script:FixturePath -ExcludeFilterType @('Invalid') } | Should -Throw '*Invalid exclude type*'
        }
    }
}

Describe 'Format-Output' {
    BeforeAll {
        $script:TestChanges = @(
            [PSCustomObject]@{ Path = 'src/alpha.ts'; Type = 'Added' }
            [PSCustomObject]@{ Path = 'src/beta.ts'; Type = 'Modified' }
            [PSCustomObject]@{ Path = 'src/gamma.ts'; Type = 'Deleted' }
        )
    }

    Context 'Plain format' {
        It 'Returns newline-separated file paths' {
            $result = Format-Output -Changes $script:TestChanges -OutputFormat 'Plain'
            $lines = $result -split [Environment]::NewLine
            $lines.Count | Should -Be 3
            $lines[0] | Should -Be 'src/alpha.ts'
        }

        It 'Excludes Type from plain output' {
            $result = Format-Output -Changes $script:TestChanges -OutputFormat 'Plain'
            $result | Should -Not -Match 'Added|Modified|Deleted'
        }
    }

    Context 'Json format' {
        It 'Returns valid JSON with Path and Type properties' {
            $result = Format-Output -Changes $script:TestChanges -OutputFormat 'Json'
            $parsed = $result | ConvertFrom-Json
            $parsed.Count | Should -Be 3
            $parsed[0].Path | Should -Be 'src/alpha.ts'
            $parsed[0].Type | Should -Be 'Added'
        }

        It 'Handles a single change entry' {
            $single = @([PSCustomObject]@{ Path = 'one.ts'; Type = 'Added' })
            $result = Format-Output -Changes $single -OutputFormat 'Json'
            $parsed = $result | ConvertFrom-Json
            # ConvertTo-Json may not wrap single object in array
            if ($parsed -is [array]) {
                $parsed[0].Path | Should -Be 'one.ts'
            }
            else {
                $parsed.Path | Should -Be 'one.ts'
            }
        }
    }

    Context 'Markdown format' {
        It 'Returns table with header row and separator' {
            $result = Format-Output -Changes $script:TestChanges -OutputFormat 'Markdown'
            $result | Should -Match '\| File \| Change Type \|'
            $result | Should -Match '\|------|-------------|'
        }

        It 'Wraps file paths in backticks within cells' {
            $result = Format-Output -Changes $script:TestChanges -OutputFormat 'Markdown'
            $result | Should -Match '`src/alpha.ts`'
        }

        It 'Includes one row per change plus header and separator' {
            $result = Format-Output -Changes $script:TestChanges -OutputFormat 'Markdown'
            $lines = $result -split [Environment]::NewLine
            $lines.Count | Should -Be 5
        }
    }

    Context 'Default format' {
        It 'Uses Plain when OutputFormat is omitted' {
            $result = Format-Output -Changes $script:TestChanges
            $lines = $result -split [Environment]::NewLine
            $lines.Count | Should -Be 3
        }
    }
}

Describe 'Invoke-ListChangedFiles' {
    It 'Returns plain output for a valid fixture file' {
        $result = Invoke-ListChangedFiles -InputPath $script:FixturePath
        $result | Should -Not -BeNullOrEmpty
    }

    It 'Filters by type when specified' {
        $result = Invoke-ListChangedFiles -InputPath $script:FixturePath -Type 'Added'
        $joined = $result -join "`n"
        $joined | Should -Match 'new-file'
        $joined | Should -Not -Match 'removed'
    }

    It 'Returns Json format when specified' {
        $result = Invoke-ListChangedFiles -InputPath $script:FixturePath -Format 'Json'
        $parsed = ($result -join "`n") | ConvertFrom-Json
        $parsed | Should -Not -BeNullOrEmpty
    }

    It 'Returns Markdown format when specified' {
        $result = Invoke-ListChangedFiles -InputPath $script:FixturePath -Format 'Markdown'
        $joined = $result -join "`n"
        $joined | Should -Match '\| File \| Change Type \|'
    }

    It 'Throws when PR reference file does not exist' {
        { Invoke-ListChangedFiles -InputPath '/nonexistent/path.xml' } | Should -Throw '*PR reference file not found*'
    }

    It 'Uses default path when InputPath is empty' {
        Mock Get-RepositoryRoot { return $script:TempDir }
        { Invoke-ListChangedFiles } | Should -Throw '*PR reference file not found*'
    }

    Context 'Multi-type filtering' {
        It 'Filters by multiple types' {
            $result = Invoke-ListChangedFiles -InputPath $script:FixturePath -Type 'Added', 'Modified'
            $joined = $result -join "`n"
            $joined | Should -Match 'new-file'
            $joined | Should -Match 'existing'
            $joined | Should -Not -Match 'removed'
        }

        It 'Filters by comma-separated type string' {
            $result = Invoke-ListChangedFiles -InputPath $script:FixturePath -Type 'Added,Renamed'
            $joined = $result -join "`n"
            $joined | Should -Match 'new-file'
            $joined | Should -Match 'new-name'
        }
    }

    Context 'ExcludeType parameter' {
        It 'Excludes deleted files' {
            $result = Invoke-ListChangedFiles -InputPath $script:FixturePath -ExcludeType 'Deleted'
            $joined = $result -join "`n"
            $joined | Should -Not -Match 'removed'
            $joined | Should -Match 'new-file'
        }

        It 'Excludes multiple types' {
            $result = Invoke-ListChangedFiles -InputPath $script:FixturePath -ExcludeType 'Deleted', 'Renamed'
            $joined = $result -join "`n"
            $joined | Should -Not -Match 'removed'
            $joined | Should -Not -Match 'new-name'
        }
    }

    Context 'Mutual exclusion' {
        It 'Throws when both Type and ExcludeType are specified' {
            { Invoke-ListChangedFiles -InputPath $script:FixturePath -Type 'Added' -ExcludeType 'Deleted' } | Should -Throw '*mutually exclusive*'
        }

        It 'Allows ExcludeType when Type is All' {
            { Invoke-ListChangedFiles -InputPath $script:FixturePath -Type 'All' -ExcludeType 'Deleted' } | Should -Not -Throw
        }
    }
}

Describe 'Entry-point execution' -Tag 'Integration' {
    BeforeAll {
        $script:ScriptPath = Join-Path $PSScriptRoot '../scripts/list-changed-files.ps1'
    }

    It 'Exits 1 when PR reference file does not exist' {
        $null = & pwsh -File $script:ScriptPath -InputPath '/nonexistent/file.xml' 2>&1
        $LASTEXITCODE | Should -Be 1
    }

    It 'Lists all files in plain format by default' {
        $output = & pwsh -File $script:ScriptPath -InputPath $script:FixturePath 2>&1
        $LASTEXITCODE | Should -Be 0
        ($output | Measure-Object).Count | Should -BeGreaterOrEqual 1
    }

    It 'Filters by Added type via parameter' {
        $output = & pwsh -File $script:ScriptPath -InputPath $script:FixturePath -Type Added 2>&1
        $LASTEXITCODE | Should -Be 0
        $joined = $output -join "`n"
        $joined | Should -Match 'new-file'
        $joined | Should -Not -Match 'removed'
    }

    It 'Filters by Deleted type via parameter' {
        $output = & pwsh -File $script:ScriptPath -InputPath $script:FixturePath -Type Deleted 2>&1
        $LASTEXITCODE | Should -Be 0
        $joined = $output -join "`n"
        $joined | Should -Match 'removed'
        $joined | Should -Not -Match 'new-file'
    }

    It 'Outputs in Json format' {
        $output = & pwsh -File $script:ScriptPath -InputPath $script:FixturePath -Format Json 2>&1
        $LASTEXITCODE | Should -Be 0
        $parsed = ($output -join "`n") | ConvertFrom-Json
        $parsed | Should -Not -BeNullOrEmpty
    }

    It 'Outputs in Markdown format with table header' {
        $output = & pwsh -File $script:ScriptPath -InputPath $script:FixturePath -Format Markdown 2>&1
        $LASTEXITCODE | Should -Be 0
        $joined = $output -join "`n"
        $joined | Should -Match '\| File \| Change Type \|'
    }

    It 'Filters by multiple types via comma-separated parameter' {
        $output = & pwsh -File $script:ScriptPath -InputPath $script:FixturePath -Type 'Added,Modified' 2>&1
        $LASTEXITCODE | Should -Be 0
        $joined = $output -join "`n"
        $joined | Should -Match 'new-file'
        $joined | Should -Match 'existing'
    }

    It 'Excludes types via ExcludeType parameter' {
        $output = & pwsh -File $script:ScriptPath -InputPath $script:FixturePath -ExcludeType Deleted 2>&1
        $LASTEXITCODE | Should -Be 0
        $joined = $output -join "`n"
        $joined | Should -Not -Match 'removed'
    }

    It 'Exits 1 when Type and ExcludeType conflict' {
        $null = & pwsh -File $script:ScriptPath -InputPath $script:FixturePath -Type Added -ExcludeType Deleted 2>&1
        $LASTEXITCODE | Should -Be 1
    }
}

Requirements and compatibility

Written for: GitHub Copilot, Claude Code

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Claude Code frontmatter

The author says: Requires git available on PATH

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Microsoft
Publisher
Microsoft (Official)
Source
microsoft/hve-core · .github/skills/shared/pr-reference · commit f425e94
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Microsoft or Microsoft.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download pr-reference.zip