使用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
项目详情
下载文件
下载适用于您的平台文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。