Skip to content

Commit 52884b5

Browse files
committed
feat(sync): add git metadata schema and sync pipeline
1 parent 59777c1 commit 52884b5

12 files changed

Lines changed: 2261 additions & 22 deletions

File tree

Cargo.lock

Lines changed: 1403 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
members = [
33
"crates/gitbase-cli",
44
"crates/gitbase-db",
5+
"crates/gitbase-git",
6+
"crates/gitbase-loader",
57
"crates/gitbase-pgwire",
68
]
79
resolver = "2"
@@ -15,9 +17,12 @@ rust-version = "1.80"
1517
anyhow = "1"
1618
async-trait = "0.1"
1719
clap = { version = "4", features = ["derive", "env"] }
20+
gix = "0.66"
1821
pgwire = "0.38"
22+
sha1 = "0.10"
1923
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio", "migrate", "macros"] }
2024
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "net"] }
2125
tracing = "0.1"
2226
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
2327
futures = "0.3"
28+
walkdir = "2"

crates/gitbase-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ path = "src/main.rs"
1111
anyhow.workspace = true
1212
clap.workspace = true
1313
gitbase-db = { path = "../gitbase-db" }
14+
gitbase-loader = { path = "../gitbase-loader" }
1415
gitbase-pgwire = { path = "../gitbase-pgwire" }
1516
tokio.workspace = true
1617
tracing.workspace = true

crates/gitbase-cli/src/main.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,27 @@ enum Commands {
2727
#[arg(long, env = "GITBASE_DB_MAX_CONNECTIONS", default_value_t = 10)]
2828
max_connections: u32,
2929
},
30+
31+
/// Sync Git metadata into PostgreSQL
32+
Sync {
33+
/// Root directories containing Git repositories
34+
#[arg(
35+
long,
36+
env = "GITBASE_REPO_ROOTS",
37+
value_delimiter = ',',
38+
num_args = 1..,
39+
default_value = "./"
40+
)]
41+
repo_roots: Vec<String>,
42+
43+
/// PostgreSQL connection string
44+
#[arg(long, env = "DATABASE_URL")]
45+
database_url: String,
46+
47+
/// Maximum database connections
48+
#[arg(long, env = "GITBASE_DB_MAX_CONNECTIONS", default_value_t = 10)]
49+
max_connections: u32,
50+
},
3051
}
3152

3253
#[tokio::main]
@@ -50,6 +71,30 @@ async fn main() -> Result<()> {
5071
let factory = Arc::new(gitbase_pgwire::GitbaseServerFactory::new(pool));
5172
gitbase_pgwire::serve(&bind, factory).await?;
5273
}
74+
Commands::Sync {
75+
repo_roots,
76+
database_url,
77+
max_connections,
78+
} => {
79+
let pool = gitbase_db::connect(&database_url, max_connections).await?;
80+
gitbase_db::health_check(&pool).await?;
81+
tracing::info!("health check passed");
82+
83+
let roots = repo_roots
84+
.iter()
85+
.map(|root| root.into())
86+
.collect::<Vec<_>>();
87+
let report = gitbase_loader::sync_repositories(&pool, &roots).await?;
88+
tracing::info!(
89+
repositories = report.repositories,
90+
refs = report.refs,
91+
commits = report.commits,
92+
commit_parents = report.commit_parents,
93+
tree_entries = report.tree_entries,
94+
files = report.files,
95+
"sync completed"
96+
);
97+
}
5398
}
5499

55100
Ok(())

crates/gitbase-db/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use anyhow::Result;
22
use sqlx::postgres::PgPoolOptions;
33
use sqlx::PgPool;
44

5+
pub mod metadata;
6+
57
/// Create a connection pool to PostgreSQL and run migrations.
68
pub async fn connect(database_url: &str, max_connections: u32) -> Result<PgPool> {
79
let pool = PgPoolOptions::new()

crates/gitbase-db/src/metadata.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
use anyhow::Result;
2+
use sqlx::PgPool;
3+
4+
#[derive(Debug, Clone)]
5+
pub struct RepositoryRecord {
6+
pub id: String,
7+
pub name: String,
8+
pub path: String,
9+
pub default_ref: Option<String>,
10+
pub is_bare: bool,
11+
}
12+
13+
#[derive(Debug, Clone)]
14+
pub struct RefRecord {
15+
pub repository_id: String,
16+
pub name: String,
17+
pub target_hash: String,
18+
pub kind: String,
19+
}
20+
21+
#[derive(Debug, Clone)]
22+
pub struct CommitRecord {
23+
pub repository_id: String,
24+
pub hash: String,
25+
pub tree_hash: String,
26+
pub author_name: Option<String>,
27+
pub author_email: Option<String>,
28+
pub committer_name: Option<String>,
29+
pub committer_email: Option<String>,
30+
pub message: Option<String>,
31+
pub committed_at_seconds: Option<i64>,
32+
}
33+
34+
#[derive(Debug, Clone)]
35+
pub struct CommitParentRecord {
36+
pub repository_id: String,
37+
pub commit_hash: String,
38+
pub parent_hash: String,
39+
pub parent_index: i32,
40+
}
41+
42+
#[derive(Debug, Clone)]
43+
pub struct TreeEntryRecord {
44+
pub repository_id: String,
45+
pub commit_hash: String,
46+
pub path: String,
47+
pub object_hash: String,
48+
pub object_type: String,
49+
pub file_mode: String,
50+
pub size: Option<i64>,
51+
}
52+
53+
#[derive(Debug, Clone)]
54+
pub struct FileRecord {
55+
pub repository_id: String,
56+
pub commit_hash: String,
57+
pub path: String,
58+
pub blob_hash: String,
59+
pub language: Option<String>,
60+
pub size: Option<i64>,
61+
pub is_binary: bool,
62+
}
63+
64+
pub async fn upsert_repository(pool: &PgPool, record: &RepositoryRecord) -> Result<()> {
65+
sqlx::query(
66+
"INSERT INTO gitbase.repositories (id, name, path, default_ref, is_bare)\
67+
VALUES ($1, $2, $3, $4, $5)\
68+
ON CONFLICT (id) DO UPDATE\
69+
SET name = EXCLUDED.name,\
70+
path = EXCLUDED.path,\
71+
default_ref = EXCLUDED.default_ref,\
72+
is_bare = EXCLUDED.is_bare",
73+
)
74+
.bind(&record.id)
75+
.bind(&record.name)
76+
.bind(&record.path)
77+
.bind(&record.default_ref)
78+
.bind(record.is_bare)
79+
.execute(pool)
80+
.await?;
81+
82+
Ok(())
83+
}
84+
85+
pub async fn upsert_ref(pool: &PgPool, record: &RefRecord) -> Result<()> {
86+
sqlx::query(
87+
"INSERT INTO gitbase.refs (repository_id, name, target_hash, kind)\
88+
VALUES ($1, $2, $3, $4)\
89+
ON CONFLICT (repository_id, name) DO UPDATE\
90+
SET target_hash = EXCLUDED.target_hash,\
91+
kind = EXCLUDED.kind",
92+
)
93+
.bind(&record.repository_id)
94+
.bind(&record.name)
95+
.bind(&record.target_hash)
96+
.bind(&record.kind)
97+
.execute(pool)
98+
.await?;
99+
100+
Ok(())
101+
}
102+
103+
pub async fn upsert_commit(pool: &PgPool, record: &CommitRecord) -> Result<()> {
104+
sqlx::query(
105+
"INSERT INTO gitbase.commits (repository_id, hash, tree_hash, author_name, author_email,\
106+
committer_name, committer_email, message, committed_at)\
107+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8,\
108+
CASE WHEN $9 IS NULL THEN NULL ELSE to_timestamp($9) END)\
109+
ON CONFLICT (repository_id, hash) DO UPDATE\
110+
SET tree_hash = EXCLUDED.tree_hash,\
111+
author_name = EXCLUDED.author_name,\
112+
author_email = EXCLUDED.author_email,\
113+
committer_name = EXCLUDED.committer_name,\
114+
committer_email = EXCLUDED.committer_email,\
115+
message = EXCLUDED.message,\
116+
committed_at = EXCLUDED.committed_at",
117+
)
118+
.bind(&record.repository_id)
119+
.bind(&record.hash)
120+
.bind(&record.tree_hash)
121+
.bind(&record.author_name)
122+
.bind(&record.author_email)
123+
.bind(&record.committer_name)
124+
.bind(&record.committer_email)
125+
.bind(&record.message)
126+
.bind(record.committed_at_seconds)
127+
.execute(pool)
128+
.await?;
129+
130+
Ok(())
131+
}
132+
133+
pub async fn upsert_commit_parent(pool: &PgPool, record: &CommitParentRecord) -> Result<()> {
134+
sqlx::query(
135+
"INSERT INTO gitbase.commit_parents (repository_id, commit_hash, parent_hash, parent_index)\
136+
VALUES ($1, $2, $3, $4)\
137+
ON CONFLICT (repository_id, commit_hash, parent_index) DO UPDATE\
138+
SET parent_hash = EXCLUDED.parent_hash",
139+
)
140+
.bind(&record.repository_id)
141+
.bind(&record.commit_hash)
142+
.bind(&record.parent_hash)
143+
.bind(record.parent_index)
144+
.execute(pool)
145+
.await?;
146+
147+
Ok(())
148+
}
149+
150+
pub async fn upsert_tree_entry(pool: &PgPool, record: &TreeEntryRecord) -> Result<()> {
151+
sqlx::query(
152+
"INSERT INTO gitbase.tree_entries (repository_id, commit_hash, path, object_hash,\
153+
object_type, file_mode, size)\
154+
VALUES ($1, $2, $3, $4, $5, $6, $7)\
155+
ON CONFLICT (repository_id, commit_hash, path) DO UPDATE\
156+
SET object_hash = EXCLUDED.object_hash,\
157+
object_type = EXCLUDED.object_type,\
158+
file_mode = EXCLUDED.file_mode,\
159+
size = EXCLUDED.size",
160+
)
161+
.bind(&record.repository_id)
162+
.bind(&record.commit_hash)
163+
.bind(&record.path)
164+
.bind(&record.object_hash)
165+
.bind(&record.object_type)
166+
.bind(&record.file_mode)
167+
.bind(record.size)
168+
.execute(pool)
169+
.await?;
170+
171+
Ok(())
172+
}
173+
174+
pub async fn upsert_file(pool: &PgPool, record: &FileRecord) -> Result<()> {
175+
sqlx::query(
176+
"INSERT INTO gitbase.files (repository_id, commit_hash, path, blob_hash, language, size, is_binary)\
177+
VALUES ($1, $2, $3, $4, $5, $6, $7)\
178+
ON CONFLICT (repository_id, commit_hash, path) DO UPDATE\
179+
SET blob_hash = EXCLUDED.blob_hash,\
180+
language = EXCLUDED.language,\
181+
size = EXCLUDED.size,\
182+
is_binary = EXCLUDED.is_binary",
183+
)
184+
.bind(&record.repository_id)
185+
.bind(&record.commit_hash)
186+
.bind(&record.path)
187+
.bind(&record.blob_hash)
188+
.bind(&record.language)
189+
.bind(record.size)
190+
.bind(record.is_binary)
191+
.execute(pool)
192+
.await?;
193+
194+
Ok(())
195+
}

crates/gitbase-git/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "gitbase-git"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
6+
[dependencies]
7+
anyhow.workspace = true
8+
gix.workspace = true
9+
sha1.workspace = true
10+
tracing.workspace = true
11+
walkdir.workspace = true

0 commit comments

Comments
 (0)