This is a user-visible change to the output of ProjectDirectory.hours_estimate(),
commit_history() and file_change_history() whenever limit is used: they now return
the number of rows that was asked for, which is generally more than before.
- FIXED:
ProjectDirectory.hours_estimate(),ProjectDirectory.commit_history()andProjectDirectory.file_change_history()divided a project-widelimitwithint(limit / len(self.repo_dirs))and handed the same truncated value to every repository. Three defects followed: anylimitbelow the repository count floored to zero and silently returned an empty frame (commit_history(limit=1)over two repositories returned no rows at all), the remainder was discarded (limit=3returned 2 rows,limit=5returned 4), and the divisor wasrepo_dirs, which is not filtered byignore_reposwhen the project is built fromRepositoryinstances, so an ignored repository still consumed a share of the limit. All three now use the same quotient-plus-remainder allocationrevs()already used: the firstlimit % len(repos)repositories receive one extra commit, and the shares are taken over theignore_repos-filteredrepos. - FIXED: Those three methods raised
ZeroDivisionErroron a project with no repositories, as didProjectDirectory.revs(num_datapoints=N). They now return the same empty DataFrame an all-failing project returns.
- FIXED:
ProjectDirectory.revs(limit=N)now distributes remainder revisions to repositories in project order instead of discarding them, so populated projects return up to the requested limit for values smaller than or not evenly divisible by the repository count.
- FIXED:
Repository.punchcard()andProjectDirectory.punchcard()now preserve finite zeros when normalizing an all-zero metric instead of producingNaNvalues through division by zero.
This is a user-visible change to the output of revs, cumulative_blame and
parallel_cumulative_blame whenever num_datapoints is used: they now return the number
of rows that was asked for, which is generally fewer than before.
- FIXED:
Repository.revs(num_datapoints=N)returned more thanNrevisions. It derivedskip = int(commit_count / N)and then kept everyskipth row, so integer truncation inflated the result — on a 10-commit historynum_datapoints=4returned 5 rows andnum_datapoints=6returned all 10. It now selects exactlymin(N, commit_count)evenly distributed revisions, keeping the existing newest-first order and always including both the newest and the oldest revision.Repository.cumulative_blame()andRepository.parallel_cumulative_blame()delegatenum_datapointstorevs(), so they no longer perform (and report) more blame work than requested. - CHANGED: A non-positive
num_datapointsnow raisesValueErrorfromRepository.revs()andProjectDirectory.revs(). Previouslynum_datapoints=0raisedZeroDivisionErrorfrom the internal skip calculation and negative values produced an arbitrary slice. - FIXED:
ProjectDirectory.revs(num_datapoints=N)dividesNacross the member repositories, which floored to zero datapoints per repository wheneverNwas smaller than the repository count. Each repository now contributes at least one revision. - Explicit
limitandskiparguments are unaffected;num_datapointsstill applies only when neither is supplied.
- CHANGED: The
pandas>=2.0.0,<3.0.0requirement is nowpandas>=2.0.0. pandas 3 has been out for a while, so the upper cap meantpip install git-pandaseither downgraded pandas or failed to resolve in any environment that already wanted pandas 3. pandas 3 requires Python >= 3.11, so resolvers on this package's declared 3.10 floor keep selecting pandas 2.x without needing an environment marker. CI now runs the suite against both majors. - FIXED:
Repository.hours_estimate()andProjectDirectory.hours_estimate()raisedTypeError: unsupported operand type(s) for *: 'datetime.datetime' and 'float'on pandas 3. The method derived per-commit timestamps withcommits.index.values.tolist(), which yields integer nanoseconds on pandas 2 butdatetime.datetimeobjects on pandas 3. It now uses(index - index[0]).total_seconds(), which is independent of the index's datetime64 resolution — relevant because pandas 3 preserves the second resolution ofto_datetime(..., unit="s")rather than always upcasting to nanoseconds. Estimated hours are unchanged on pandas 2. - FIXED:
plot_lifeline()raisedAttributeError: 'StringArray' object has no attribute 'sort'on pandas 3.Series.unique()returns a NumPy array on pandas 2 but aStringArrayunder pandas 3's defaultstrdtype, and only the former supports in-place.sort(). The unique filenames are now sorted withsorted().
- CHANGED (user-visible):
redisandcoverageare no longer installed bypip install git-pandas. Neither is used by the core library —redisis imported behind a guard ingitpandas.cacheandcoverageis imported lazily insideRepository.coverage()— so a plain install no longer pulls in a Redis client and a test-coverage tool. They are now optional extras: installgit-pandas[redis]to useRedisDFCache, andgit-pandas[coverage]to useRepository.coverage()/Repository.file_change_rates(coverage=True). Both are included in theallanddevextras. Anyone relying onRedisDFCacheafter a barepip install git-pandasmust now installgit-pandas[redis]; the failure mode is the existing explicitImportError("Need redis installed to use redis cache"). - FIXED:
Repository.coverage()swallowed a missingcoveragepackage. Theimport coveragesat inside atrywhose final handler is a broadexcept Exception, so on a repository that does have a.coveragefile the result was an empty DataFrame — indistinguishable from "no coverage data". The import now happens outside that block and raises anImportErrornaming thegit-pandas[coverage]extra.Repository.file_change_rates(coverage=True)re-raises it rather than returning an empty frame. Genuine "no coverage data" cases still return the empty DataFrame as before.
This is a user-visible change to the output of cumulative_blame and
parallel_cumulative_blame. Callers that index those frames positionally (iloc[0],
columns[0]) will see different data.
- FIXED:
Repository.cumulative_blame()andRepository.parallel_cumulative_blame()returned the stringrepositorylabel column (and anylabels_to_addcolumns) mixed in with the per-contributor LOC counts, sodf.sum(axis=1)— total LOC over time — raisedTypeError: can only concatenate str (not "int") to str. Label columns are now dropped, so the returned columns are exactly the contributor names, matching the columnsProjectDirectory.cumulative_blame()already returned. - FIXED: All three cumulative blame methods (
Repository.cumulative_blame(),Repository.parallel_cumulative_blame(),ProjectDirectory.cumulative_blame()) returned a reverse-chronological (monotonically decreasing)DatetimeIndex, becauserevs()yields newest-first and nothing sorted. That made label-based date slicing raiseKeyError: Value based partial slicing on non-monotonic DatetimeIndexes..., silently negated.diff(), and made.iloc[-1]the oldest row. The index is now sorted ascending. - ENHANCED: The zero-column filter in both
Repositorymethods now does a numeric sum instead of an O(n) string concatenation over the label column. - CHANGED: When several revisions share one timestamp,
ProjectDirectory.cumulative_blame()now consistently keeps the newest of them (index.duplicated(keep="last")on the ascending index). Previously a single-repository project kept the newest and a multi-repository project kept the oldest, because only the latter was index-sorted by the merge.
- NEW:
Repository.safe_fetch_remote()- Safely fetch changes from remote repositories without modifying working directory- Read-only operation with comprehensive error handling
- Support for dry-run preview and remote validation
- Configurable remote names and pruning options
- NEW:
Repository.warm_cache()- Pre-populate repository cache for improved performance- Configurable method selection with intelligent parameter handling
- Performance metrics and cache entry tracking
- Significant performance improvements (1.5-10x speedup demonstrated)
- NEW:
ProjectDirectory.bulk_fetch_and_warm()- Efficiently process multiple repositories- Parallel processing support when joblib is available
- Error isolation (failures in one repo don't affect others)
- Comprehensive summary statistics and progress tracking
- NEW:
CacheEntryclass with metadata tracking (timestamps, age calculation) - ENHANCED: Thread-safe cache operations with proper locking mechanisms
- ENHANCED: Cache key consistency improvements using
||delimiter format - ENHANCED: Cache timestamp and metadata access methods (
get_cache_info(),list_cached_keys())
- NEW: Comprehensive remote operations documentation (
docs/source/remote_operations.rst) - NEW: Cache warming and remote fetch example (
examples/remote_fetch_and_cache_warming.py) - NEW: Cache timestamp usage example (
examples/cache_timestamps.py) - NEW: Release analytics example (
examples/release_analytics.py)
- FIXED:
Repository.commits_in_tags()never walked history. It returned exactly one row per tag — the tagged commit — rather than attributing every commit to the release that shipped it, as documented. The backwards walk now runs, so each commit is attributed to the first tag that contains it, stopping at the previous tag, at thestart/endbounds, or at a root commit. The walk uses an explicit worklist, so a release spanning more commits than the interpreter's recursion limit no longer raisesRecursionError. - FIXED: Tag lookup used positional
Series.__getitem__, which emitted a pandasFutureWarningand would become aKeyErroron a future pandas release.
Note: This is a user-visible output change. commits_in_tags() — and ProjectDirectory.commits_in_tags(), which aggregates it — now return one row per commit instead of one row per tag. Code that counted rows to count releases needs updating; count distinct values of the tag column instead.
- FIXED:
ProjectDirectoryandGitHubProfilenow let each repository auto-detectmainormasterby default, so mixed-branch projects include every repository in branch-based history metrics. Passing an explicitdefault_branchcontinues to force that branch across all repositories.
Note: Repositories with neither a main nor master branch are now skipped with a warning during project initialization. Previously they were included but returned empty branch-based history.
- FIXED:
Repository.cumulative_blame()andparallel_cumulative_blame()no longer duplicate rows when multiple commits share a timestamp.
- FIXED:
Repository.file_change_history()now reports actual per-file insertions and deletions, including root commits, which restores non-zero churn metrics infile_change_rates().
- FIXED:
ProjectDirectory.punchcard()now returns a well-formed empty DataFrame when no repository yields commit data and avoids pandas aggregationFutureWarnings.
- FIXED:
Repository.file_owner()always read the commit's committer regardless of thecommitterflag, socommitter=Falsereturned the top committer under a column labelledauthor. It now selects the identity to match the flag, matchingRepository.blame(). This also fixesRepository.file_detail(committer=False)andProjectDirectory.file_detail(committer=False), whosefile_ownercolumn reported the committer on rebased, cherry-picked, squash-merged, or web-UI-committed history.
- FIXED:
serialize_pandas_object()dropped every non-datetime DataFrame index duringorient="records"conversion, so index-carried identity disappeared from tool results (e.g. a blame-shaped frame serialized as[{"loc": 7}]without thecommitter/authorit belongs to). Named index levels, includingMultiIndexlevels such asfileand(tag_date, commit_date), are now materialized as columns. - FIXED: Serialization mutated the DataFrame it was given — rewriting a
DatetimeIndexinto formatted strings and replacing datetime columns in place — which corrupted shared cached frames. Serialization now works on a copy and leaves its input untouched. - FIXED: Serializing a frame that keeps a column and index level of the same name (e.g.
file_change_history()'sdate) raisedValueError: cannot insert date, already exists.
- FIXED:
GitHubProfilenow follows GitHub API pagination links and requests up to 100 repositories per page. If any page fails, discovery returns an empty profile instead of silently analyzing partial results.
- FIXED:
Repository.hours_estimate()now includes the first-commit allowance, increasing estimates bysingle_commit_hoursper contributor and giving single-commit contributors a non-zero estimate.
- FIXED:
ProjectDirectory.blame()now preserves committer/author and file grouping keys when combining multiple repositories. Contributors are aggregated by name across repositories,blame(by="file")no longer raisesKeyError, and project-level bus factors are calculated from contributors instead of row numbers.
- FIXED:
ProjectDirectory.blame(by="file")grouped on(committer/author, file)only. Becausefileis a repository-relative path, files sharing a path across repositories (README.md,setup.py,__init__.py, ...) were silently summed into a single row reporting a line count that matched neither repository, and the originating repository was discarded entirely. The repository is now part of the grouping, so the result is indexed by(committer/author, file, repository)— matching the identification thatfile_detail(),file_change_rates(), andbus_factor(by="file")already carry.blame(by="repository")is unchanged; aggregating a contributor across repositories there remains correct.
Note: This adds an index level to blame(by="file") output. Code that indexes that result by (committer, file) needs updating.
- FIXED:
Repository.cumulative_blame()reshaped therevs()frame it received in place — adding a column per committer, deletingrev, and replacing the index with the commit dates. Because a cache backend hands out the stored DataFrame by reference, a laterrevs()call returned that wrecked frame, and a secondcumulative_blame()call raisedValueError: Internal Error: self.revs() returned DataFrame without 'rev' column.(parallel_cumulative_blame()swallowed the same failure and returned an empty DataFrame). It now works on a copy. - FIXED:
ProjectDirectory.cumulative_blame()renamed each member repository's cachedcumulative_blame()columns in place while suffixing them with the repository name, so a later per-repository call returnedAlice__repo1instead ofAlice. The suffixing now happens on copies.
- FIXED:
ProjectDirectory.bus_factor(by="repository")passed the caller'sinclude_globsinto each repository'signore_globsargument. The caller'signore_globswas silently discarded andinclude_globsdoubled as an exclusion list, so the bus factor was computed over the wrong set of files — typically excluding exactly the files the caller asked to include. (by="file"was already correct.)
- FIXED:
ProjectDirectory.blame()andfile_detail()raisedAttributeError: 'NoneType' object has no attribute 'reset_index'when the project contained no repositories, or when every repository raisedGitCommandError(repositories with no commits, or a branch that doesn't exist). Both now return a well-formed empty DataFrame matching their normal output shape, as sibling methods such asfile_change_history()already did. - FIXED:
ProjectDirectory.commits_in_tags()raisedValueError: No objects to concatenatewhen no repository yielded tagged commits. It now returns an empty DataFrame carrying the usualcommit_sha,tag, andrepositorycolumns and the(tag_date, commit_date)index.
- FIXED:
Repository.punchcard()wroteday_of_weekandhour_of_daycolumns straight into thecommit_history()frame it was handed. Because a cache backend returns the stored DataFrame by reference, every latercommit_history()call then returned those two extra columns. The same in-place write affectedProjectDirectory.commit_history(),file_detail(), andrevs(), which added arepositorycolumn to borrowed per-repository frames. All of these now copy before writing.
- FIXED:
Repository.warm_cache()silently substitutedlimit=100forcommit_historyandfile_change_rates, so it populated a cache key that no ordinary call would ever hit — the defaultwarm_cache()gave those two methods no speedup whatsoever. The injected limit is gone, so warming populates the keys callers actually use. - FIXED:
hours_estimate(),punchcard(),cumulative_blame(), andparallel_cumulative_blame()resolvedbranch=Noneto the default branch before delegating tocommit_history()/revs(). The inner call therefore keyed on"master"while a user's owncommit_history()keyed onNone, storing several copies of the same history and defeating cache reuse between them. The caller'sbranchvalue is now passed through unchanged; resolution happens only where a branch name is genuinely needed.
- FIXED:
@multicachereturned a bare wrapper, so every decoratedRepositorymethod lost its name, docstring, and signature.help(),inspect.signature(), and any reflection over the API saw(self, *args, **kwargs). The decorator now appliesfunctools.wraps, which restores introspection and — because the MCP server generates its tool schemas by reflecting overRepository— restores the real parameter names and defaults in every MCP tool schema.
- FIXED:
Repository.invalidate_cache()now targets the method-first cache key layout, processes every key when combined with a pattern, and returns accurate removal counts for in-memory and Redis backends. - FIXED:
@multicachebuilt cache keys fromkwargsonly, so any argument passed positionally was invisible to the key and collapsed toNone. Keys are now resolved against the decorated method's signature (inspect.signature().bind()+apply_defaults()), so positional and keyword calls key identically. This fixes:Repository(working_dir=<master-only repo>, cache_backend=...)raisingValueError: Could not detect default branch— the internalhas_branch("main")/has_branch("master")probes shared one key.file_detail()reporting the first file's owner for every file — the internalfile_owner(rev, file_path, ...)calls shared one key.
- FIXED:
blame()'skey_listmisspelledignore_globsasignore_blobs, soignore_globsnever contributed to the cache key and all variants collided.blame(ignore_globs=[...])andbus_factor(ignore_globs=[...])now return the same results with and without a cache backend. - FIXED:
skip_brokenwas missing from the cache key offile_change_history,file_change_rates,revs,cumulative_blame,parallel_cumulative_blame, andtags, soskip_broken=Trueandskip_broken=Falseshared an entry. - FIXED: Cache key parts are now joined with
||instead of_, which could occur inside the values themselves (e.g.get_file_content(path="docs", rev="release_2")collided withpath="docs_release", rev="2"). - NEW:
@multicachevalidateskey_listagainst the decorated method's signature at decoration time and raisesValueErroron a name that isn't a parameter, turning the typo class of bug into a loud failure.
Note: The cache key format has changed. Stale EphemeralCache/DiskCache entries simply miss and recompute, but RedisDFCache users sharing a cache across versions should flush it (or use a new key prefix) to avoid retaining entries under the old format.
- NEW: 38 comprehensive tests for remote operations and cache warming
- NEW: Thread safety tests for cache operations
- NEW: Edge case and error handling test coverage
- NEW: Regression coverage for every fix above, including cached-vs-uncached parity, multi-repository aggregation against repositories that share file paths, and cache-immutability checks that fail if a method writes into a frame it was handed
- NEW: Exact rev-to-rev regression tests pinning churn, file, elapsed-time, author, and committer values, and covering
include_globs/ignore_globsthroughrelease_tag_summary() - IMPROVED: Overall test coverage and reliability
- FIXED: Various minor bugs and future warnings
- No API was removed, renamed, or given a new required argument; every existing call still runs.
- Three changes are user-visible in the data returned, and are called out in full above:
commits_in_tags()now returns one row per commit rather than one row per tag.blame(by="file")on aProjectDirectorygains arepositoryindex level.- The cache key format changed;
RedisDFCacheusers sharing a cache across versions should flush it.
- Beyond those, results change only where they were previously wrong — the bug fixes above return corrected numbers for churn, hours, ownership, bus factor, and blame. Analyses pinned to specific values from an earlier release should expect them to move.
- Existing cache backends work seamlessly with new features.
- Significant caching bugfixes and updates
- Added a DiskCache that persists across runs
- Added release analytics
- Updated coverage file parsing to use coverage.py API instead of direct file parsing
- Added coverage>=5.0.0 as a core dependency
- Added a basic MCP server
- Added methods to
Repositoryfor getting files in repo, getting content of a file, and getting diffs of a revision
- Docs CI bugfix
- Support for default branch setting instead of assuming master, will infer if not passed
- Better handling of ignore repos in project directory setup
- Added a branch exists helper in repository
- Docs corrections
- Imrpoved test suite
- Many bugfixes
- Updates for pandas v2
- Fully transitioned to ignore_globs and include_globs style syntax
- Parallelized cumulative blame support with joblib threading backend
- Added threading parallelism to many project directory functions.
- Added a chaching module for optional redis or memory backed caching of certain resultsets
- Added ignore_globs option alongside all methods with ignore_dir and extensions, will be the only method for filtering files in v2.0.0
- _repo_name changed to repo_name in project directories (old method left with deprecation warning)
- repo_name property added to repositories
- Support for estimating time spent developing on projects.
- bugfix in ignore_dir option for root level directories
- file details function
- Stable API
- Punchcard dataframe added
- Plotting helpers added to library under utilities module
- Added github.com profile object
- Added file owner utility
- Added lifelines example
- Added rev to file change history table
- Added file-wise blame using by='file' parameter
- Bus Factor returns a dataframe
- Now supporting python 2.7+ and 3.3+
- Added file change rates table with risk metrics
- Added basic functionality with coverage files
- Added limited time window based dataset functionality
- Expanded docs
- Added cumulative blame and revision history
- Added approximate bus factor analysis
- Added blame
- Initial release, basic interface to commit history and descriptors