Skip to main content

xlgit

Beta (0.1.0). Every release is fuzz-tested against thousands of real-world workbooks, but you'll still find cases it gets wrong. Git keeps every version, so a bad merge can always be undone. Please open an issue when something looks wrong.

Git and GitHub treat .xlsx as an opaque binary blob. You can commit, fork and branch it, but a diff just says "binary file changed" and any merge where both sides touched the file is a conflict. This fixes that.

What you get

Without xlgit With xlgit
git diff Binary files differ changed Budget!B2 1000 -> 1100, plus inserted, deleted and moved rows, and chart, image and comment changes
Seeing changes n/a xlgit diff --html: the sheet as a grid in your browser, changes highlighted
Merge, different cells edited conflict, pick one whole file merges cleanly
Merge, one side inserted rows conflict the other side's edits follow their rows
Merge, same cell edited conflict conflict on just that cell, listed in a _merge_conflicts sheet with a link to it
Charts, images, comments, formatting, macros n/a kept, and their edits to them carried over
Tables and pivot tables n/a merged: you add rows, they add a column, you get both
Pull request on GitHub "binary file not shown" bot comment with a table of every changed cell, chart, table and pivot

Formulas are compared as formulas (=B2+C2), not their cached results.

Get started

You need git. Then, once per computer, either:

With Python (3.9 or newer):

pip install xlgit
xlgit install

Without Python: download the program for your computer from the latest release (xlgit-windows.exe, xlgit-macos-arm64 for Apple silicon, xlgit-macos-intel, xlgit-linux), rename it to xlgit (xlgit.exe on Windows), put it somewhere it will stay, and run xlgit install. On Windows you can also just double-click it and it offers to set itself up. The downloads aren't code-signed yet: on Windows click "More info" then "Run anyway"; on a Mac, right-click it, choose Open, then Open again. If you move the program later, run xlgit install again.

That's it. Every git repository on this computer now understands .xlsx and .xlsm files, including ones you clone or create later. Keep using git the way you already do.

To also get a comment listing the changed cells on every GitHub pull request, run this once inside the repository and push:

xlgit install --github

Run xlgit on its own at any time to see the commands and check that everything is set up.

Everyday use

See what changed. xlgit diff lists every cell that changed in your workbooks since the last commit. Inserted, deleted and moved rows show up as rows, not as every cell below them changing. git diff, git log -p and git show show cell changes too.

$ xlgit diff
=== budget.xlsx ===
changed        Budget!C3  350 -> 999
row inserted   Budget row 4  A: 'Gas', B: 60, C: 70, D: =B4+C4
changed        Budget!D8  =SUM(D2:D6) -> =SUM(D2:D7)

xlgit diff --html opens the same changes in your browser, laid out like the spreadsheet:

Visual diff: a changed cell shows its old value, an inserted row is green, a changed SUM range is highlighted

Merge. git merge and git pull combine edits from both branches cell by cell. If you changed different cells, there's nothing to do.

When both of you changed the same cell, git stops and xlgit tells you which cells:

xlgit merged budget.xlsx: took 3 cell(s) from the other branch, but 1 change(s) clash.
  Budget B2: yours 1100, theirs 1200 (was 1000)
  Your values were kept. Every clash is listed, with a link, on the sheet '_merge_conflicts'.

Open the workbook, go through the _merge_conflicts sheet (each row links to its cell), fix the cells, delete that sheet, save, then run git add budget.xlsx and git commit. Git keeps every version, so nothing is ever lost: git merge --abort undoes the whole merge.

Found a problem?

Please open an issue. Bug reports on real workbooks are the most useful thing you can give this project, and you don't have to share your data to do it:

xlgit scrub --merge budget.xlsx     # during a merge that went wrong: base, yours, theirs, in one zip
xlgit scrub old.xlsx new.xlsx       # any workbooks, e.g. for a wrong diff

scrub makes copies where every number, piece of text, comment, chart label and file property is replaced with made-up values, keeping formulas, layout, charts, tables and pivots, so the problem still shows up. Equal values stay equal across the files scrubbed together. Sheet names and named ranges are kept (formulas refer to them), macros are removed, and images are replaced with blank ones. Open the copies and check them before you share them.

How the merge works

A workbook is a zip of XML files: one per sheet, one per chart, one per image and so on. Instead of re-saving the whole thing through a spreadsheet library (which is how charts used to get lost), xlgit starts from your copy's zip and only rewrites the XML that has to change:

  • Cells: 3-way merge cell by cell. A cell only one side changed takes that side's value.
  • Rows: if one branch inserted, deleted or moved rows and the other edited cells, the edits land on the rows where their cells ended up, with formula references renumbered the way Excel does it. An edit to a row the other branch deleted is a conflict.
  • Charts, images, comments, macros: 3-way merge object by object. If only their branch changed a chart, you get their version. If both did, yours is kept and it's flagged as a conflict. Chart edits caused by cell changes (Excel caches plotted values inside the chart) don't count as edits.
  • Tables: merged field by field. Their new column plus your new rows gives a table with both. Tables added on their branch come over, and if both branches added a Table2, theirs is renamed Table3 and their formulas are updated to match.
  • Pivot tables: a pivot, its data cache and the cached records merge as one bundle. A branch that only refreshed a pivot (same layout, new data) doesn't count as an edit, so your data change and their "switch to Average" merge cleanly. New pivots from their branch come over, sharing an existing cache when they used one.
  • Sheets: new sheets on their branch come over whole, with their charts, tables and pivots. Renames and deletes merge too.
  • Named ranges: merged by name.

Writers renumber a workbook's internal files on every save (add a chart to an early sheet and every later chart1.xml becomes chart2.xml). xlgit matches objects by what they are, like "the chart called Chart 1 on sheet Notes" or "table id 3", not by file name, so renumbering doesn't cause false conflicts.

Excel recalculates every formula and refreshes affected pivot tables when it opens the merged file.

Commands

xlgit                        help, and whether everything is set up
xlgit install                set up every repository on this computer (once)
xlgit install --github       add pull request comments to this repository
xlgit install --repo         set up only this repository
xlgit uninstall [--repo]     undo the setup
xlgit diff                   what changed in your workbooks since the last commit
xlgit diff FILE              ... in one workbook
xlgit diff OLD NEW           compare any two workbooks (--markdown for a table)
xlgit diff --html [FILES]    open the changes in your browser (--out=page.html to save it)
xlgit scrub FILE...          copies with every value made up, safe to attach to a bug report
xlgit scrub --merge FILE     the three versions of a merge that went wrong, scrubbed, in one zip
xlgit --version

Git runs xlgit textconv and xlgit merge itself; you don't need to.

Don't want a package? xlgit.py is a single file. Copy it in, pip install openpyxl lxml, and run python xlgit.py install.

The GitHub Action

xlgit install --github writes this workflow. You can also add it by hand:

# .github/workflows/excel-diff.yml
name: Excel diff
on:
  pull_request:
    paths: ["**/*.xlsx", "**/*.xlsm"]
permissions:
  contents: read
  pull-requests: write
jobs:
  excel-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: MichaelFowler1/excel-git@v0.1.0

It keeps one comment per pull request up to date as you push. Pull requests from forks can't be commented on with GitHub's default token, so for those the changed cells go in the run's summary page instead.

Tests

pip install -r requirements-dev.txt
python -m pytest tests

The tests build workbook versions with charts, comments and named ranges, run real git merge through the driver, and check every object survived. Set XLGIT_KEEP=some/dir to keep the merged files and open them in Excel yourself.

Fuzzing against real workbooks

fuzz/merge_fuzz.py runs the merge over a folder of real spreadsheets. For each one it makes two branches with random cell edits, merges them, and checks the result: it opens, every edit from both sides is there with its exact type and value, nothing else changed, no chart, table or pivot was lost, and the conflicts reported are exactly the cells both sides changed.

pip install py7zr
fuzz/fetch_corpus.sh corpus            # ~17,000 workbooks: Enron corpus + open-source test suites
python fuzz/merge_fuzz.py corpus --keep failed/

--keep saves the base, ours, theirs and merged files of every failure. --rounds N tries N different random edits per workbook.

Limits

  • Formatting changes their branch made to existing cells come over only when both branches have the same set of styles. Otherwise your formatting is kept.
  • Column widths, merged cells and conditional formatting on existing sheets aren't merged. Yours are kept.
  • Until Excel refreshes a merged pivot, the numbers in its cells are the old ones. Excel does this on open, but tools that read the file without Excel (pandas, openpyxl) see the stale values.
  • Slicers, timelines and tables linked to external data connections aren't merged. They're reported as conflicts so nothing disappears silently.
  • If both branches added a chart to a sheet that had none, only yours is kept (flagged).
  • Inserted and deleted rows are followed when only one branch changed a sheet's rows. If both branches inserted or deleted rows on the same sheet, that sheet merges cell by cell, and cells that only moved can show up as conflicts.
  • Rows are recognised by their contents. A row change can't always be recognised (say, deleting one of many identical rows); then the sheet merges cell by cell and anything unclear is reported as a conflict, never guessed.
  • Inserted columns show as changed cells for now.
  • .xls (the old pre-2007 format) isn't supported. Save as .xlsx.

Roadmap

Next up: testing against thousands of real workbooks, understanding inserted and moved rows, then code review for spreadsheets: showing what a change does to the numbers, tests that run on every pull request, and a linter. See ROADMAP.md.

License

Apache License 2.0. See LICENSE.

You can use, change and ship xlgit, including commercially. If you pass it on, modified or not, keep the NOTICE file and the copyright line at the top of xlgit.py with it. The license doesn't grant use of the xlgit name for your own version.

Created by Michael Fowler.

Release files for xlgit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xlgit 0.1.0
File Size Uploaded
xlgit-0.1.0.tar.gz 72.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for xlgit 0.1.0
File Interpreter ABI Platform
xlgit-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 125.5 kB

Release files / xlgit-0.1.0.tar.gz

Download URL xlgit-0.1.0.tar.gz
Size 72.2 kB
Tags Source
SHA-256 checksum
How to use checksums
7dbf7d9409e95b0dcb4b2f927753b4304943f0893f38f5c38ae8bcb3c6c52486
BLAKE2b-256 checksum
How to use checksums
1d7ac85035e6b1bb02a91c2018aa3bb908248d2aaa24cd4f8a76f3888f557dc1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release files / xlgit-0.1.0-py3-none-any.whl

Download URL xlgit-0.1.0-py3-none-any.whl
Size 53.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4034616fd072870facf9a75f8affd757369de95ea749b9cf1cddc689e7f79507
BLAKE2b-256 checksum
How to use checksums
20354a43f9e02091fb647e492322e6fbd4b75add4e4ee8ffb128a0efd725a98f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page