使用SQLite分析Git历史的工具
项目描述
git历史记录
使用SQLite分析Git历史的工具
关于此项目的背景信息,请参阅 git历史记录:使用Git和SQLite收集的数据分析工具
安装
使用pip
安装此工具
$ pip install git-history
演示
git-history-demos.datasette.io托管使用此工具创建的三个示例数据库
- pge-outages展示了PG&E(电力供应商)停电的历史,使用simonw/pge-outages中的数据,通过pge-outages.sh转换
- ca-fires展示了加利福尼亚州在fire.ca.gov/incidents上报告的火灾历史,使用来自simonw/ca-fires-history的数据,通过ca-fires.sh转换
- sf-bay-511记录了旧金山湾区的交通和事故数据,来自511.org,通过dbreunig/511-events-history收集,通过sf-bay-511.sh转换
演示是通过Datasette在Google Cloud Run上由此GitHub Actions工作流程部署的。
使用方法
此工具可用于针对包含JSON、CSV/TSV或其他格式文件以及多个版本在Git历史中跟踪的Git仓库。阅读Git scraping: track changes over time by scraping to a Git repository了解如何创建此类仓库。
file
命令分析仓库中单个文件的历史,并生成一个SQLite数据库表,表示该文件随时间的变化版本。
假设该文件包含多个对象,例如,抓取停电地图的结果或记录满的CSV文件。
假设您有一个名为incidents.json
的文件,它是一个对象的JSON数组,该文件在仓库中有多个版本。该文件的每个版本可能看起来像这样
[
{
"IncidentID": "abc123",
"Location": "Corner of 4th and Vermont",
"Type": "fire"
},
{
"IncidentID": "cde448",
"Location": "555 West Example Drive",
"Type": "medical"
}
]
将工作目录切换到相关GitHub仓库,并运行以下命令
git-history file incidents.db incidents.json
这将创建一个名为incidents.db
的新SQLite数据库,包含三个表
commits
表包含每行提交,有一个hash
列、commit_at
日期以及一个到namespace
的外键。item
表包含每个版本中filename.json
文件的每行记录,有一个额外的_commit
列,是该表到commit
表的外键。namespaces
表包含一行。这允许您使用下面描述的--namespace
选项为不同的文件构建多个表。
此示例的数据库模式如下所示
CREATE TABLE [namespaces] (
[id] INTEGER PRIMARY KEY,
[name] TEXT
);
CREATE UNIQUE INDEX [idx_namespaces_name]
ON [namespaces] ([name]);
CREATE TABLE [commits] (
[id] INTEGER PRIMARY KEY,
[namespace] INTEGER REFERENCES [namespaces]([id]),
[hash] TEXT,
[commit_at] TEXT
);
CREATE UNIQUE INDEX [idx_commits_namespace_hash]
ON [commits] ([namespace], [hash]);
CREATE TABLE [item] (
[IncidentID] TEXT,
[Location] TEXT,
[Type] TEXT
);
如果您有10个历史版本的incidents.json
文件,并且每个文件包含30个事件,您将在item
表中得到10 * 30 = 300行。
使用ID跟踪单个项目的历史
如果您的对象具有唯一标识符或多个列的组合形成一个唯一标识符,您可以使用--id
选项来消除重复并跟踪每个项目随时间的变化。
这为应用此工具提供了一种更有趣的方法。
如果有一个名为IncidentID
的唯一标识符列,您可以运行以下命令
git-history file incidents.db incidents.json --id IncidentID
这里使用的数据库模式与未使用--id
选项时使用的模式非常不同。
如果您已导入历史记录,则该命令将跳过它已经看到的任何提交,并仅处理新的提交。这意味着尽管初始导入可能很慢,但后续导入应该会运行得更快。
该命令将创建六个表 - commits
、item
、item_version
、columns
、item_changed
和namespaces
。
以下是完整的模式
CREATE TABLE [namespaces] (
[id] INTEGER PRIMARY KEY,
[name] TEXT
);
CREATE UNIQUE INDEX [idx_namespaces_name]
ON [namespaces] ([name]);
CREATE TABLE [commits] (
[id] INTEGER PRIMARY KEY,
[namespace] INTEGER REFERENCES [namespaces]([id]),
[hash] TEXT,
[commit_at] TEXT
);
CREATE UNIQUE INDEX [idx_commits_namespace_hash]
ON [commits] ([namespace], [hash]);
CREATE TABLE [item] (
[_id] INTEGER PRIMARY KEY,
[_item_id] TEXT
, [IncidentID] TEXT, [Location] TEXT, [Type] TEXT, [_commit] INTEGER);
CREATE UNIQUE INDEX [idx_item__item_id]
ON [item] ([_item_id]);
CREATE TABLE [item_version] (
[_id] INTEGER PRIMARY KEY,
[_item] INTEGER REFERENCES [item]([_id]),
[_version] INTEGER,
[_commit] INTEGER REFERENCES [commits]([id]),
[IncidentID] TEXT,
[Location] TEXT,
[Type] TEXT,
[_item_full_hash] TEXT
);
CREATE TABLE [columns] (
[id] INTEGER PRIMARY KEY,
[namespace] INTEGER REFERENCES [namespaces]([id]),
[name] TEXT
);
CREATE UNIQUE INDEX [idx_columns_namespace_name]
ON [columns] ([namespace], [name]);
CREATE TABLE [item_changed] (
[item_version] INTEGER REFERENCES [item_version]([_id]),
[column] INTEGER REFERENCES [columns]([id]),
PRIMARY KEY ([item_version], [column])
);
CREATE VIEW item_version_detail AS select
commits.commit_at as _commit_at,
commits.hash as _commit_hash,
item_version.*,
(
select json_group_array(name) from columns
where id in (
select column from item_changed
where item_version = item_version._id
)
) as _changed_columns
from item_version
join commits on commits.id = item_version._commit;
CREATE INDEX [idx_item_version__item]
ON [item_version] ([_item]);
item表
item
表将包含每行的最新版本,通过ID消除重复,并包含以下附加列
_id
- 一个数字整数主键,用作从item_version
表的外键。_item_id
- 使用命令的--id
选项指定的列值的哈希。在处理新版本时用于消除重复。_commit
- 到commit
表的外键,表示修改此项目的最新提交。
item_version表
item_version
表将包含每个捕获的不同版本的行,以及以下列
_id
- 项目版本记录的数字ID。_item
- 到item
表的外键。_version
- 数字版本号,从1开始,对每个捕获的版本递增。_commit
- 到commit
表的外键。_item_full_hash
- 项目的此版本的哈希。此工具内部使用以识别提交之间已更改的项目。
该表中的其他列代表自上一版本以来发生变化的原数据中的列。如果值没有变化,它将用 null
表示。
如果一个值之前已设置但已改回 null
,则在 item_version
行中它仍将表示为 null
。您可以使用以下描述的 item_changed
多对多表来识别这些。
您可以使用 --full-versions
选项在每个版本存储项目的完整副本,而不是仅存储已更改的列。
item_version_detail 视图
此 SQL 视图将 item_version
与 commits
连接,以添加三个额外的列:带有提交日期的 _commit_at
和带有 Git 提交哈希的 _commit_hash
。
item_changed
此多对多表指示 item_version
中确切地更改了哪些列。
item_version
是item_version
表中一行的外键。column
是columns
表中一行的外键。
此表将有最多的行数,因此它仅存储两个整数以节省空间。
columns
columns
表存储列名。它由 item_changed
引用。
id
- 整数 ID。name
- 列的名称。namespace
- 外键到namespaces
,如果多个文件历史记录共享相同的数据库。
保留的列名
请注意,_id
、_item_full_hash
、_item
、_item_id
、_version
、_commit
、_item_id
、_commit_at
、_commit_hash
、_changed_columns
、rowid
被认为是此工具的保留列名。
如果您的数据包含这些中的任何一项,它们将被重命名,以添加尾随下划线,例如 _id_
、_item_
、_version_
,以避免与保留列冲突。
如果您有一个名为 _commit_
的列,它也将被重命名,添加额外的尾随下划线,因此 _commit_
变为 _commit__
,_commit__
变为 _commit___
。
附加选项
--repo DIRECTORY
- 如果不是当前工作目录,则为 Git 仓库的路径。--branch TEXT
- 要分析的 Git 分支 - 默认为main
。--id TEXT
- 如上所述:传递一个或多个唯一标识记录的列,以便可以随时间计算该记录的变化。--full-versions
- 而不是在item_version
表记录中仅记录已更改的列,记录每个版本的完整副本。--ignore TEXT
- 要忽略的一列或多列 - 它们将不包括在结果数据库中。--csv
- 将数据视为 CSV 或 TSV 而不是 JSON,并尝试猜测正确的方言。--dialect
- 使用特定的 CSV 方言。选项为excel
、excel-tab
和unix
- 有关详细信息,请参阅 Python CSV 文档。--skip TEXT
- 要跳过的一个或多个完整 Git 提交哈希。如果您的修订历史中的一些数据损坏,防止此工具工作,则可以使用此选项。--start-at TEXT
- 跳过指定提交哈希之前的提交。--start-after TEXT
- 跳过包括指定提交哈希在内的提交,然后从下一个提交开始处理。--convert TEXT
- 用于转换的自定义 Python 代码,如下所述。--import TEXT
- 为--convert
导入的附加 Python 模块。--ignore-duplicate-ids
- 如果一个文件的单个版本中具有相同的 ID 多次,则工具将带错误退出。使用此选项忽略此并选择两个重复项中的第一个。--namespace TEXT
- 如果您希望将多个不同文件的历史记录包含在同一数据库中,请使用此选项。默认值为item
,但您可以将其设置为其他值,这将生成名为yournamespace
和yournamespace_version
的表格。--wal
- 在创建的数据库文件上启用 WAL 模式。如果您计划在git-history
创建数据库的同时对数据库运行查询,请使用此选项。--silent
- 不显示进度条。
CSV 和 TSV 数据
如果您的存储库中的数据是 CSV 或 TSV 文件,您可以通过添加 --csv
选项来处理它。这将尝试检测文件使用的分隔符,因此相同的选项适用于逗号分隔和制表符分隔的值。
git-history file trees.db trees.csv --id TreeID
您还可以使用 --dialect
选项指定 CSV 语法。
使用 --convert 进行自定义转换
如果您的数据不是 CSV/TSV 或平面 JSON 数组,您可以使用 --convert
选项对其进行重塑。
此工具需要的格式是字典数组,如上面所示的 incidents.json
示例。
如果您的数据不适合此形状,您可以提供一段 Python 代码,将每个存储文件的磁盘内容转换为 Python 字典列表。
例如,如果您的存储文件每个都像这样
{
"incidents": [
{
"id": "552",
"name": "Hawthorne Fire",
"engines": 3
},
{
"id": "556",
"name": "Merlin Fire",
"engines": 1
}
]
}
您可以使用以下 Python 片段将它们转换为所需的格式
json.loads(content)["incidents"]
(默认情况下,将 json
模块暴露给您的自定义函数。)
然后您将像这样运行工具
git-history file database.db incidents.json \
--id id \
--convert 'json.loads(content)["incidents"]'
content
变量始终是一个表示仓库历史中特定时刻文件内容的 bytes
对象。
您可以使用 --import
导入额外的模块。以下示例显示了如何使用 --import
读取使用 ;
作为分隔符的 CSV 文件
git-history file trees.db ../sf-tree-history/Street_Tree_List.csv \
--repo ../sf-tree-history \
--import csv \
--import io \
--convert '
fp = io.StringIO(content.decode("utf-8"))
return list(csv.DictReader(fp, delimiter=";"))
' \
--id TreeID
您可以使用 --import xml.etree.ElementTree
导入嵌套模块,例如 ElementTree,然后在函数体中引用它们为 xml.etree.ElementTree
。例如,如果您的跟踪数据在看起来像这样的 items.xml
文件中
<items>
<item id="1" name="One" />
<item id="2" name="Two" />
<item id="3" name="Three" />
</item>
您可以使用以下 --convert
脚本来加载它
git-history file items.xml --convert '
tree = xml.etree.ElementTree.fromstring(content)
return [el.attrib for el in tree.iter("item")]
' --import xml.etree.ElementTree --id id
如果您的 Python 代码跨越多行,则需要包含一个 return
语句。
您还可以在 --convert
代码中使用 Python 生成器,例如
git-history file stats.db package-stats/stats.json \
--repo package-stats \
--convert '
data = json.loads(content)
for key, counts in data.items():
for date, count in counts.items():
yield {
"package": key,
"date": date,
"count": count
}
' --id package --id date
此转换函数期望数据看起来像这样
{
"airtable-export": {
"2021-05-18": 66,
"2021-05-19": 60,
"2021-05-20": 87
}
}
开发
要为此工具做出贡献,首先检出代码。然后创建一个新的虚拟环境
cd git-history
python -m venv venv
source venv/bin/activate
或者如果您正在使用 pipenv
pipenv shell
现在安装依赖关系并测试依赖关系
pip install -e '.[test]'
要运行测试
pytest
要更新本 README 文件中的模式示例
cog -r README.md
项目详情
下载文件
下载适用于您的平台文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。