Last active
August 6, 2025 14:23
-
-
Save YarGnawh/8853cd13fcc498feb91d94a96bec48aa to your computer and use it in GitHub Desktop.
Azure DevOps TFVC Changeset Diff Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Azure DevOps TFVC Changeset Diff Generator | |
# | |
# A bash script that generates unified diff patches from Azure DevOps TFVC (Team Foundation Version Control) changesets. This tool solves the common problem of extracting readable diffs from TFVC changesets, which Azure DevOps doesn't provide natively through # its API. | |
# | |
# Features | |
# | |
# - Fetches all files modified in a specific TFVC changeset | |
# - Retrieves both current and previous versions of each file | |
# - Generates a standard unified diff patch file | |
# - Handles text files, binary files, additions, and deletions | |
# - Outputs to a single .patch file that can be reviewed or applied elsewhere | |
# | |
# Use Cases | |
# | |
# - Code review outside of Azure DevOps | |
# - Archiving changeset modifications | |
# - Migrating TFVC history to Git | |
# - Analyzing changes across multiple files in a changeset | |
# - Creating portable change documentation | |
# | |
# Requirements | |
# | |
# - Bash shell | |
# - curl for API calls | |
# - jq for JSON parsing | |
# - Azure DevOps Personal Access Token (PAT) with TFVC read permissions | |
# | |
# Usage | |
# | |
# ./get_changeset_diff.sh <changeset_id> | |
# | |
# Outputs to: changeset_<id>.patch | |
# | |
# Note | |
# | |
# Update the script with your Azure DevOps organization, project, and PAT before use. | |
# | |
# --- | |
# This script fills a gap in Azure DevOps TFVC tooling by providing a simple way to extract and review changeset differences in a standard format. | |
set -e | |
# Configuration | |
PAT="generate in azure devops" | |
ORGANIZATION="org name" | |
PROJECT="project name" | |
API_VERSION="7.1" | |
BASE_URL="https://dev.azure.com/${ORGANIZATION}" | |
# Check arguments | |
if [ $# -ne 1 ]; then | |
echo "Usage: $0 <changeset_id>" | |
exit 1 | |
fi | |
CHANGESET_ID=$1 | |
PREVIOUS_CHANGESET=$((CHANGESET_ID - 1)) | |
OUTPUT_FILE="changeset_${CHANGESET_ID}.patch" | |
# Create temp directory for files | |
TEMP_DIR=$(mktemp -d) | |
trap "rm -rf ${TEMP_DIR}" EXIT | |
echo "Fetching changeset ${CHANGESET_ID} details..." | |
# Get list of changes in the changeset | |
CHANGES_JSON=$(curl -s -u ":${PAT}" \ | |
"${BASE_URL}/_apis/tfvc/changesets/${CHANGESET_ID}/changes?api-version=${API_VERSION}") | |
# Parse the changes and get file paths | |
echo "${CHANGES_JSON}" | jq -r '.value[]? | select(.item.path != null) | .item.path' > "${TEMP_DIR}/changed_files.txt" | |
if [ ! -s "${TEMP_DIR}/changed_files.txt" ]; then | |
echo "No file changes found in changeset ${CHANGESET_ID}" | |
exit 1 | |
fi | |
echo "Found $(wc -l < ${TEMP_DIR}/changed_files.txt) changed files" | |
echo "Generating diff patch..." | |
# Initialize patch file with header | |
cat > "${OUTPUT_FILE}" << EOF | |
From changeset ${CHANGESET_ID} | |
Date: $(date) | |
Subject: Changes from changeset ${CHANGESET_ID} | |
--- | |
EOF | |
# Process each changed file | |
while IFS= read -r FILE_PATH; do | |
echo "Processing: ${FILE_PATH}" | |
# Don't URL encode - use the path as-is with proper query params | |
# Azure DevOps API expects the path parameter to be part of the query string | |
# Create subdirectories in temp | |
FILE_NAME=$(basename "${FILE_PATH}") | |
SAFE_NAME=$(echo "${FILE_PATH}" | sed 's/[^a-zA-Z0-9._-]/_/g') | |
# Fetch current version (changeset version) | |
# Note: The path parameter needs to be URL encoded as a query parameter | |
CURRENT_FILE="${TEMP_DIR}/current_${SAFE_NAME}" | |
ENCODED_PATH=$(printf '%s' "$FILE_PATH" | jq -sRr @uri) | |
curl -s -u ":${PAT}" \ | |
"${BASE_URL}/_apis/tfvc/items?path=${ENCODED_PATH}&versionType=Changeset&version=${CHANGESET_ID}&api-version=${API_VERSION}" \ | |
-H "Accept: application/octet-stream" \ | |
-o "${CURRENT_FILE}" 2>/dev/null || true | |
# Fetch previous version | |
PREVIOUS_FILE="${TEMP_DIR}/previous_${SAFE_NAME}" | |
curl -s -u ":${PAT}" \ | |
"${BASE_URL}/_apis/tfvc/items?path=${ENCODED_PATH}&versionType=Changeset&version=${PREVIOUS_CHANGESET}&api-version=${API_VERSION}" \ | |
-H "Accept: application/octet-stream" \ | |
-o "${PREVIOUS_FILE}" 2>/dev/null || true | |
# Check if files are binary or if previous doesn't exist (new file) | |
if [ ! -f "${PREVIOUS_FILE}" ] || [ ! -s "${PREVIOUS_FILE}" ]; then | |
# New file | |
echo "" >> "${OUTPUT_FILE}" | |
echo "diff --git a${FILE_PATH} b${FILE_PATH}" >> "${OUTPUT_FILE}" | |
echo "new file mode 100644" >> "${OUTPUT_FILE}" | |
echo "--- /dev/null" >> "${OUTPUT_FILE}" | |
echo "+++ b${FILE_PATH}" >> "${OUTPUT_FILE}" | |
if [ -f "${CURRENT_FILE}" ] && file "${CURRENT_FILE}" | grep -q text; then | |
# Add content with + prefix for new files | |
sed 's/^/+/' "${CURRENT_FILE}" >> "${OUTPUT_FILE}" | |
else | |
echo "Binary file added" >> "${OUTPUT_FILE}" | |
fi | |
elif [ ! -f "${CURRENT_FILE}" ] || [ ! -s "${CURRENT_FILE}" ]; then | |
# Deleted file | |
echo "" >> "${OUTPUT_FILE}" | |
echo "diff --git a${FILE_PATH} b${FILE_PATH}" >> "${OUTPUT_FILE}" | |
echo "deleted file mode 100644" >> "${OUTPUT_FILE}" | |
echo "--- a${FILE_PATH}" >> "${OUTPUT_FILE}" | |
echo "+++ /dev/null" >> "${OUTPUT_FILE}" | |
if file "${PREVIOUS_FILE}" | grep -q text; then | |
# Add content with - prefix for deleted files | |
sed 's/^/-/' "${PREVIOUS_FILE}" >> "${OUTPUT_FILE}" | |
else | |
echo "Binary file deleted" >> "${OUTPUT_FILE}" | |
fi | |
else | |
# Check if files are text files | |
if file "${CURRENT_FILE}" | grep -q text && file "${PREVIOUS_FILE}" | grep -q text; then | |
# Generate unified diff | |
echo "" >> "${OUTPUT_FILE}" | |
echo "diff --git a${FILE_PATH} b${FILE_PATH}" >> "${OUTPUT_FILE}" | |
echo "--- a${FILE_PATH}" >> "${OUTPUT_FILE}" | |
echo "+++ b${FILE_PATH}" >> "${OUTPUT_FILE}" | |
# Use diff to generate the actual differences | |
diff -u "${PREVIOUS_FILE}" "${CURRENT_FILE}" | tail -n +3 >> "${OUTPUT_FILE}" 2>/dev/null || true | |
else | |
# Binary files | |
echo "" >> "${OUTPUT_FILE}" | |
echo "diff --git a${FILE_PATH} b${FILE_PATH}" >> "${OUTPUT_FILE}" | |
echo "Binary files differ" >> "${OUTPUT_FILE}" | |
fi | |
fi | |
done < "${TEMP_DIR}/changed_files.txt" | |
echo "" | |
echo "✅ Diff patch saved to: ${OUTPUT_FILE}" | |
echo "Total size: $(wc -l < ${OUTPUT_FILE}) lines" | |
# Optional: Display summary | |
echo "" | |
echo "Summary of changes:" | |
echo "${CHANGES_JSON}" | jq -r '.value[]? | "\(.changeType): \(.item.path)"' 2>/dev/null || true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment