branch_base 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/ci.yaml +76 -0
- data/.gitignore +19 -0
- data/.prettierignore +4 -0
- data/.rspec +5 -0
- data/.rubocop.yml +259 -0
- data/Dockerfile +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +124 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/Rakefile +13 -0
- data/branch_base.gemspec +43 -0
- data/internal/git-wrapped.png +0 -0
- data/internal/screenshot.jpg +0 -0
- data/internal/screenshot.png +0 -0
- data/internal/template.html.erb +84 -0
- data/lib/branch_base/cli.rb +79 -0
- data/lib/branch_base/database.rb +98 -0
- data/lib/branch_base/repository.rb +43 -0
- data/lib/branch_base/sync.rb +211 -0
- data/lib/branch_base/version.rb +5 -0
- data/lib/branch_base.rb +97 -0
- data/package.json +13 -0
- data/scripts/branch_base +6 -0
- data/scripts/release_branch_base.sh +22 -0
- data/yarn.lock +20 -0
- metadata +297 -0
data/branch_base.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "lib/branch_base/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "branch_base"
|
6
|
+
spec.version = BranchBase::VERSION
|
7
|
+
spec.authors = ["Shayon Mukherjee"]
|
8
|
+
spec.email = ["shayonj@gmail.com"]
|
9
|
+
|
10
|
+
spec.summary = "Sync Git Repository into a SQLite Database"
|
11
|
+
spec.description =
|
12
|
+
"BranchBase provides a CLI to synchronize a Git repository into a SQLite database."
|
13
|
+
spec.homepage = "https://github.com/shayonj/branch_base"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.7.0"
|
16
|
+
|
17
|
+
spec.files =
|
18
|
+
`git ls-files -z`.split("\x0")
|
19
|
+
.reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
spec.bindir = "scripts"
|
21
|
+
spec.executables = ["branch_base"]
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_dependency("rugged")
|
25
|
+
spec.add_dependency("sqlite3")
|
26
|
+
spec.add_dependency("thor", "~> 1.0")
|
27
|
+
|
28
|
+
spec.metadata = { "rubygems_mfa_required" => "true" }
|
29
|
+
|
30
|
+
spec.add_development_dependency("git")
|
31
|
+
spec.add_development_dependency("prettier_print")
|
32
|
+
spec.add_development_dependency("pry")
|
33
|
+
spec.add_development_dependency("rake")
|
34
|
+
spec.add_development_dependency("rspec")
|
35
|
+
spec.add_development_dependency("rubocop")
|
36
|
+
spec.add_development_dependency("rubocop-packaging")
|
37
|
+
spec.add_development_dependency("rubocop-performance")
|
38
|
+
spec.add_development_dependency("rubocop-rake")
|
39
|
+
spec.add_development_dependency("rubocop-rspec")
|
40
|
+
spec.add_development_dependency("syntax_tree")
|
41
|
+
spec.add_development_dependency("syntax_tree-haml")
|
42
|
+
spec.add_development_dependency("syntax_tree-rbs")
|
43
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,84 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8" />
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6
|
+
<title>Git Wrapped Statistics - <%= @repo_name.capitalize %> </title>
|
7
|
+
<style>
|
8
|
+
body {
|
9
|
+
font-family: "Fira Code", monospace;
|
10
|
+
background-color: #0C0A00;
|
11
|
+
color: #c1c1c1;
|
12
|
+
margin: 0;
|
13
|
+
padding: 0;
|
14
|
+
font-weight: 100;
|
15
|
+
}
|
16
|
+
|
17
|
+
header {
|
18
|
+
background-color: #0C0A00;
|
19
|
+
color: #fff;
|
20
|
+
text-align: center;
|
21
|
+
padding: 20px;
|
22
|
+
border-bottom: 1px solid #333;
|
23
|
+
}
|
24
|
+
|
25
|
+
h1 {
|
26
|
+
font-size: 30px;
|
27
|
+
margin: 0;
|
28
|
+
}
|
29
|
+
|
30
|
+
.container {
|
31
|
+
max-width: 1200px;
|
32
|
+
margin: 20px auto;
|
33
|
+
background-color: #0C0A00;
|
34
|
+
border-radius: 10px;
|
35
|
+
overflow: hidden;
|
36
|
+
display: grid;
|
37
|
+
grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
|
38
|
+
gap: 20px;
|
39
|
+
}
|
40
|
+
|
41
|
+
.section {
|
42
|
+
padding: 20px;
|
43
|
+
border-bottom: 1px solid #333;
|
44
|
+
}
|
45
|
+
|
46
|
+
h2 {
|
47
|
+
font-size: 20px;
|
48
|
+
margin: 0 0 20px;
|
49
|
+
color: #fff;
|
50
|
+
}
|
51
|
+
|
52
|
+
ul {
|
53
|
+
/* list-style: none; */
|
54
|
+
padding: 0;
|
55
|
+
}
|
56
|
+
|
57
|
+
li {
|
58
|
+
margin-bottom: 10px;
|
59
|
+
font-size: 18px;
|
60
|
+
}
|
61
|
+
</style>
|
62
|
+
<link
|
63
|
+
rel="stylesheet"
|
64
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/firacode/5.2.0/fira_code.css"
|
65
|
+
/>
|
66
|
+
</head>
|
67
|
+
<body>
|
68
|
+
<header>
|
69
|
+
<h1>Git Wrapped Statistics - <%= @repo_name.capitalize %></h1>
|
70
|
+
</header>
|
71
|
+
<div class="container">
|
72
|
+
<% @results.each do |key, values| %>
|
73
|
+
<div class="section">
|
74
|
+
<h2><%= @emojis[key] %> <%= key.gsub('_', ' ').split.map(&:capitalize).join(' ') %></h2>
|
75
|
+
<ul>
|
76
|
+
<% values.each do |item| %>
|
77
|
+
<li><%= item[0] %>: <%= item[1] %><%= item[2].nil? ? "" : "(#{item[2]})"%></li>
|
78
|
+
<% end %>
|
79
|
+
</ul>
|
80
|
+
</div>
|
81
|
+
<% end %>
|
82
|
+
</div>
|
83
|
+
</body>
|
84
|
+
</html>
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "erb"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module BranchBase
|
8
|
+
class CLI < Thor
|
9
|
+
desc "sync REPO_PATH", "Synchronize a Git directory to a SQLite database"
|
10
|
+
def sync(repo_path)
|
11
|
+
BranchBase.logger.info("Starting sync process for #{repo_path}...")
|
12
|
+
|
13
|
+
full_repo_path = File.expand_path(repo_path)
|
14
|
+
|
15
|
+
unless File.directory?(File.join(full_repo_path, ".git"))
|
16
|
+
BranchBase.logger.error(
|
17
|
+
"The specified path is not a valid Git repository: #{full_repo_path}",
|
18
|
+
)
|
19
|
+
exit(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
repo_name = File.basename(full_repo_path)
|
23
|
+
db_directory = full_repo_path
|
24
|
+
db_filename = File.join(db_directory, "#{repo_name}_git_data.db")
|
25
|
+
|
26
|
+
database = Database.new(db_filename)
|
27
|
+
repository = Repository.new(full_repo_path)
|
28
|
+
start_time = Time.now
|
29
|
+
sync = Sync.new(database, repository)
|
30
|
+
|
31
|
+
sync.run
|
32
|
+
elapsed_time = Time.now - start_time
|
33
|
+
BranchBase.logger.info(
|
34
|
+
"Repository data synced successfully in #{db_filename} in #{elapsed_time.round(2)} seconds",
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "git-wrapped REPO_PATH",
|
39
|
+
"Generate Git wrapped statistics for the given repository"
|
40
|
+
def git_wrapped(repo_path)
|
41
|
+
BranchBase.logger.info("Generating Git wrapped for #{repo_path}...")
|
42
|
+
|
43
|
+
full_repo_path = File.expand_path(repo_path)
|
44
|
+
@repo_name = File.basename(full_repo_path)
|
45
|
+
db_filename = File.join(full_repo_path, "#{@repo_name}_git_data.db")
|
46
|
+
|
47
|
+
unless File.exist?(db_filename)
|
48
|
+
BranchBase.logger.error("Database file not found: #{db_filename}")
|
49
|
+
exit(1)
|
50
|
+
end
|
51
|
+
|
52
|
+
database = Database.new(db_filename)
|
53
|
+
@results = BranchBase.execute_git_wrapped_queries(database)
|
54
|
+
@emojis = BranchBase.emojis_for_titles
|
55
|
+
|
56
|
+
json_full_path = "#{full_repo_path}/git-wrapped.json"
|
57
|
+
File.write(json_full_path, JSON.pretty_generate(@results))
|
58
|
+
BranchBase.logger.info("Git wrapped JSON stored in #{json_full_path}")
|
59
|
+
|
60
|
+
erb_template = File.read("./internal/template.html.erb")
|
61
|
+
erb = ERB.new(erb_template)
|
62
|
+
generated_html = erb.result(binding)
|
63
|
+
|
64
|
+
html_full_path = "#{full_repo_path}/git-wrapped.html"
|
65
|
+
File.write(html_full_path, generated_html)
|
66
|
+
|
67
|
+
BranchBase.logger.info("Git wrapped HTML stored in #{html_full_path}")
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "version", "Prints the version"
|
71
|
+
def version
|
72
|
+
puts BranchBase::VERSION
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.exit_on_failure?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sqlite3"
|
4
|
+
|
5
|
+
module BranchBase
|
6
|
+
class Database
|
7
|
+
def initialize(db_path)
|
8
|
+
@db = SQLite3::Database.new(db_path)
|
9
|
+
setup_schema
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(query, *params)
|
13
|
+
@db.execute(query, *params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def prepare(statement)
|
17
|
+
@db.prepare(statement)
|
18
|
+
end
|
19
|
+
|
20
|
+
def transaction(&block)
|
21
|
+
@db.transaction(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def last_insert_row_id
|
25
|
+
@db.last_insert_row_id
|
26
|
+
end
|
27
|
+
|
28
|
+
def setup_schema
|
29
|
+
@db.execute_batch(<<-SQL)
|
30
|
+
CREATE TABLE IF NOT EXISTS repositories (
|
31
|
+
repo_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
32
|
+
name TEXT NOT NULL,
|
33
|
+
url TEXT NOT NULL
|
34
|
+
);
|
35
|
+
|
36
|
+
CREATE TABLE IF NOT EXISTS commits (
|
37
|
+
commit_hash TEXT PRIMARY KEY,
|
38
|
+
repo_id INTEGER NOT NULL,
|
39
|
+
author TEXT NOT NULL,
|
40
|
+
committer TEXT NOT NULL,
|
41
|
+
message TEXT NOT NULL,
|
42
|
+
timestamp TEXT NOT NULL,
|
43
|
+
FOREIGN KEY (repo_id) REFERENCES repositories (repo_id)
|
44
|
+
);
|
45
|
+
|
46
|
+
CREATE TABLE IF NOT EXISTS branches (
|
47
|
+
branch_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
48
|
+
repo_id INTEGER NOT NULL,
|
49
|
+
name TEXT NOT NULL,
|
50
|
+
FOREIGN KEY (repo_id) REFERENCES repositories (repo_id)
|
51
|
+
);
|
52
|
+
|
53
|
+
CREATE TABLE IF NOT EXISTS branch_commits (
|
54
|
+
branch_id INTEGER NOT NULL,
|
55
|
+
commit_hash TEXT NOT NULL,
|
56
|
+
PRIMARY KEY (branch_id, commit_hash),
|
57
|
+
FOREIGN KEY (branch_id) REFERENCES branches (branch_id),
|
58
|
+
FOREIGN KEY (commit_hash) REFERENCES commits (commit_hash)
|
59
|
+
);
|
60
|
+
|
61
|
+
CREATE TABLE IF NOT EXISTS files (
|
62
|
+
file_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
63
|
+
repo_id INTEGER NOT NULL,
|
64
|
+
file_path TEXT NOT NULL,
|
65
|
+
latest_commit TEXT NOT NULL,
|
66
|
+
FOREIGN KEY (repo_id) REFERENCES repositories (repo_id),
|
67
|
+
FOREIGN KEY (latest_commit) REFERENCES commits (commit_hash)
|
68
|
+
);
|
69
|
+
|
70
|
+
CREATE TABLE IF NOT EXISTS commit_files (
|
71
|
+
commit_hash TEXT NOT NULL,
|
72
|
+
file_id INTEGER NOT NULL,
|
73
|
+
changes TEXT NOT NULL,
|
74
|
+
PRIMARY KEY (commit_hash, file_id),
|
75
|
+
FOREIGN KEY (commit_hash) REFERENCES commits (commit_hash),
|
76
|
+
FOREIGN KEY (file_id) REFERENCES files (file_id)
|
77
|
+
);
|
78
|
+
|
79
|
+
CREATE TABLE IF NOT EXISTS commit_parents (
|
80
|
+
commit_hash TEXT NOT NULL,
|
81
|
+
parent_hash TEXT NOT NULL,
|
82
|
+
PRIMARY KEY (commit_hash, parent_hash),
|
83
|
+
FOREIGN KEY (commit_hash) REFERENCES commits (commit_hash),
|
84
|
+
FOREIGN KEY (parent_hash) REFERENCES commits (commit_hash)
|
85
|
+
);
|
86
|
+
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_commits_repo_id ON commits (repo_id);
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_commits_author ON commits (author);
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_commits_committer ON commits (committer);
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_branches_repo_id ON branches (repo_id);
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_files_repo_id ON files (repo_id);
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_files_file_path ON files (file_path);
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_commit_parents_commit_hash ON commit_parents (commit_hash);
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_commit_parents_parent_hash ON commit_parents (parent_hash);
|
95
|
+
SQL
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rugged"
|
4
|
+
|
5
|
+
module BranchBase
|
6
|
+
class Repository
|
7
|
+
attr_reader :repo
|
8
|
+
|
9
|
+
def initialize(repo_path)
|
10
|
+
@repo = Rugged::Repository.new(repo_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def walk(branch_name = nil, &block)
|
14
|
+
# Use the provided branch's head commit OID if a branch name is given,
|
15
|
+
# otherwise, use the repository's HEAD commit OID.
|
16
|
+
oid =
|
17
|
+
if branch_name
|
18
|
+
branch = @repo.branches[branch_name]
|
19
|
+
raise ArgumentError, "Branch not found: #{branch_name}" unless branch
|
20
|
+
branch.target.oid
|
21
|
+
else
|
22
|
+
@repo.head.target.oid
|
23
|
+
end
|
24
|
+
|
25
|
+
@repo.walk(oid, Rugged::SORT_TOPO, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_branch_name
|
29
|
+
head_ref = @repo.head.name
|
30
|
+
head_ref.sub(%r{^refs/heads/}, "")
|
31
|
+
rescue Rugged::ReferenceError
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def path
|
36
|
+
@repo.path
|
37
|
+
end
|
38
|
+
|
39
|
+
def branches
|
40
|
+
@repo.branches
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BranchBase
|
4
|
+
class Sync
|
5
|
+
# TODO acctualy see if bulk inserts are faster
|
6
|
+
BATCH_SIZE = 1000
|
7
|
+
|
8
|
+
def initialize(database, repository)
|
9
|
+
@db = database
|
10
|
+
@repo = repository
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
@db.execute("PRAGMA foreign_keys = OFF")
|
15
|
+
|
16
|
+
repo_id = sync_repository
|
17
|
+
sync_branches(repo_id)
|
18
|
+
sync_commits(repo_id)
|
19
|
+
|
20
|
+
@db.execute("PRAGMA foreign_keys = ON")
|
21
|
+
end
|
22
|
+
|
23
|
+
def sync_repository
|
24
|
+
repo_path = @repo.path.chomp(".git/")
|
25
|
+
repo_name = File.basename(repo_path)
|
26
|
+
|
27
|
+
existing_repo_id =
|
28
|
+
@db.execute(
|
29
|
+
"SELECT repo_id FROM repositories WHERE url = ?",
|
30
|
+
[repo_path],
|
31
|
+
).first
|
32
|
+
return existing_repo_id[0] if existing_repo_id
|
33
|
+
|
34
|
+
@db.execute(
|
35
|
+
"INSERT INTO repositories (name, url) VALUES (?, ?)",
|
36
|
+
[repo_name, repo_path],
|
37
|
+
)
|
38
|
+
@db.last_insert_row_id
|
39
|
+
end
|
40
|
+
|
41
|
+
def sync_branches(repo_id)
|
42
|
+
BranchBase.logger.debug("Syncing branches for repository ID: #{repo_id}")
|
43
|
+
|
44
|
+
default_branch_name = @repo.default_branch_name
|
45
|
+
return unless default_branch_name
|
46
|
+
|
47
|
+
@repo.branches.each do |branch|
|
48
|
+
next if branch.name.nil? || branch.target.nil?
|
49
|
+
|
50
|
+
branch_id = insert_branch(repo_id, branch.name)
|
51
|
+
|
52
|
+
# Only sync commits for the default branch
|
53
|
+
if branch.name == default_branch_name
|
54
|
+
insert_branch_commits(branch_id, branch)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def insert_branch(repo_id, branch_name)
|
60
|
+
existing_branch_id =
|
61
|
+
@db.execute(
|
62
|
+
"SELECT branch_id FROM branches WHERE name = ? AND repo_id = ?",
|
63
|
+
[branch_name, repo_id],
|
64
|
+
).first
|
65
|
+
return existing_branch_id[0] if existing_branch_id
|
66
|
+
|
67
|
+
@db.execute(
|
68
|
+
"INSERT INTO branches (repo_id, name) VALUES (?, ?)",
|
69
|
+
[repo_id, branch_name],
|
70
|
+
)
|
71
|
+
@db.last_insert_row_id
|
72
|
+
end
|
73
|
+
|
74
|
+
def insert_branch_commits(branch_id, branch)
|
75
|
+
BranchBase.logger.debug("Syncing branch commits for: #{branch.name}")
|
76
|
+
|
77
|
+
head_commit = branch.target
|
78
|
+
walker = Rugged::Walker.new(@repo.repo)
|
79
|
+
walker.push(head_commit)
|
80
|
+
|
81
|
+
walker.each do |commit|
|
82
|
+
next if commit_exists?(commit.oid)
|
83
|
+
|
84
|
+
@db.execute(
|
85
|
+
"INSERT OR IGNORE INTO branch_commits (branch_id, commit_hash) VALUES (?, ?)",
|
86
|
+
[branch_id, commit.oid],
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def sync_commits(repo_id)
|
92
|
+
batched_commits = []
|
93
|
+
batched_files = []
|
94
|
+
|
95
|
+
@repo.walk do |commit|
|
96
|
+
next if commit_exists?(commit.oid)
|
97
|
+
|
98
|
+
batched_commits << [
|
99
|
+
commit.oid,
|
100
|
+
repo_id,
|
101
|
+
commit.author[:name],
|
102
|
+
commit.committer[:name],
|
103
|
+
commit.message,
|
104
|
+
commit.time.to_s,
|
105
|
+
]
|
106
|
+
|
107
|
+
if batched_commits.size >= BATCH_SIZE
|
108
|
+
insert_commits(batched_commits)
|
109
|
+
batched_commits.clear
|
110
|
+
end
|
111
|
+
|
112
|
+
commit.diff.each_patch do |patch|
|
113
|
+
file_path = patch.delta.new_file[:path]
|
114
|
+
batched_files << [repo_id, file_path, commit.oid, patch.to_s]
|
115
|
+
|
116
|
+
if batched_files.size >= BATCH_SIZE
|
117
|
+
insert_files_and_commit_files(batched_files)
|
118
|
+
batched_files.clear
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
insert_commit_parents(commit)
|
123
|
+
end
|
124
|
+
|
125
|
+
insert_commits(batched_commits) unless batched_commits.empty?
|
126
|
+
insert_files_and_commit_files(batched_files) unless batched_files.empty?
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def commit_exists?(commit_hash)
|
132
|
+
@db.execute(
|
133
|
+
"SELECT COUNT(*) FROM commits WHERE commit_hash = ?",
|
134
|
+
[commit_hash],
|
135
|
+
).first[
|
136
|
+
0
|
137
|
+
].positive?
|
138
|
+
end
|
139
|
+
|
140
|
+
def insert_commit_files(commit, repo_id)
|
141
|
+
commit.diff.each_patch do |patch|
|
142
|
+
file_path = patch.delta.new_file[:path]
|
143
|
+
@db.execute(
|
144
|
+
"INSERT OR IGNORE INTO files (repo_id, file_path, latest_commit) VALUES (?, ?, ?)",
|
145
|
+
[repo_id, file_path, commit.oid],
|
146
|
+
)
|
147
|
+
file_id = @db.last_insert_row_id
|
148
|
+
@db.execute(
|
149
|
+
"INSERT INTO commit_files (commit_hash, file_id, changes) VALUES (?, ?, ?)",
|
150
|
+
[commit.oid, file_id, patch.to_s],
|
151
|
+
)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def insert_commit_parents(commit)
|
156
|
+
BranchBase.logger.debug(
|
157
|
+
"Inserting parent commits for repository: #{@repo.path}",
|
158
|
+
)
|
159
|
+
|
160
|
+
commit.parent_ids.each do |parent_id|
|
161
|
+
@db.execute(
|
162
|
+
"INSERT INTO commit_parents (commit_hash, parent_hash) VALUES (?, ?)",
|
163
|
+
[commit.oid, parent_id],
|
164
|
+
)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def insert_branches(batched_branches)
|
169
|
+
@db.transaction do
|
170
|
+
batched_branches.each do |data|
|
171
|
+
@db.execute(
|
172
|
+
"INSERT OR IGNORE INTO branches (repo_id, name, head_commit) VALUES (?, ?, ?)",
|
173
|
+
data,
|
174
|
+
)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def insert_commits(batched_commits)
|
180
|
+
BranchBase.logger.debug(
|
181
|
+
"Inserting commits for repository ID: #{@repo.path}",
|
182
|
+
)
|
183
|
+
|
184
|
+
@db.transaction do
|
185
|
+
batched_commits.each do |data|
|
186
|
+
@db.execute(
|
187
|
+
"INSERT INTO commits (commit_hash, repo_id, author, committer, message, timestamp) VALUES (?, ?, ?, ?, ?, ?)",
|
188
|
+
data,
|
189
|
+
)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def insert_files_and_commit_files(batched_data)
|
195
|
+
@db.transaction do
|
196
|
+
batched_data.each do |data|
|
197
|
+
repo_id, file_path, commit_hash, changes = data
|
198
|
+
@db.execute(
|
199
|
+
"INSERT OR IGNORE INTO files (repo_id, file_path, latest_commit) VALUES (?, ?, ?)",
|
200
|
+
[repo_id, file_path, commit_hash],
|
201
|
+
)
|
202
|
+
file_id = @db.last_insert_row_id
|
203
|
+
@db.execute(
|
204
|
+
"INSERT INTO commit_files (commit_hash, file_id, changes) VALUES (?, ?, ?)",
|
205
|
+
[commit_hash, file_id, changes],
|
206
|
+
)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
data/lib/branch_base.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "json"
|
5
|
+
require "branch_base/version"
|
6
|
+
require "branch_base/database"
|
7
|
+
require "branch_base/repository"
|
8
|
+
require "branch_base/sync"
|
9
|
+
require "branch_base/cli"
|
10
|
+
|
11
|
+
module BranchBase
|
12
|
+
def self.logger
|
13
|
+
@logger ||=
|
14
|
+
Logger
|
15
|
+
.new($stdout)
|
16
|
+
.tap do |log|
|
17
|
+
log.progname = "BranchBase"
|
18
|
+
|
19
|
+
log.level = ENV["DEBUG"] ? Logger::DEBUG : Logger::INFO
|
20
|
+
|
21
|
+
log.formatter =
|
22
|
+
proc do |severity, datetime, progname, msg|
|
23
|
+
"#{datetime}: #{severity} - #{progname}: #{msg}\n"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.execute_git_wrapped_queries(database)
|
29
|
+
queries = {
|
30
|
+
"top_contributors_of_the_year" =>
|
31
|
+
"SELECT author, COUNT(*) AS commit_count
|
32
|
+
FROM commits
|
33
|
+
WHERE substr(commits.timestamp, 1, 4) = '2023'
|
34
|
+
GROUP BY author
|
35
|
+
ORDER BY commit_count DESC
|
36
|
+
LIMIT 10;
|
37
|
+
",
|
38
|
+
"commits_per_day_of_the_week" =>
|
39
|
+
"SELECT
|
40
|
+
CASE strftime('%w', substr(timestamp, 1, 10))
|
41
|
+
WHEN '0' THEN 'Sunday'
|
42
|
+
WHEN '1' THEN 'Monday'
|
43
|
+
WHEN '2' THEN 'Tuesday'
|
44
|
+
WHEN '3' THEN 'Wednesday'
|
45
|
+
WHEN '4' THEN 'Thursday'
|
46
|
+
WHEN '5' THEN 'Friday'
|
47
|
+
WHEN '6' THEN 'Saturday'
|
48
|
+
END as day_of_week,
|
49
|
+
COUNT(*) as commit_count
|
50
|
+
FROM commits
|
51
|
+
WHERE substr(timestamp, 1, 4) = '2023'
|
52
|
+
GROUP BY day_of_week
|
53
|
+
ORDER BY commit_count DESC;
|
54
|
+
",
|
55
|
+
"most_active_months" =>
|
56
|
+
"SELECT substr(commits.timestamp, 1, 7) AS month, COUNT(*) AS commit_count
|
57
|
+
FROM commits
|
58
|
+
WHERE substr(commits.timestamp, 1, 4) = '2023'
|
59
|
+
GROUP BY month
|
60
|
+
ORDER BY commit_count DESC
|
61
|
+
LIMIT 12;
|
62
|
+
",
|
63
|
+
"commits_with_most_significant_number_of_changes" =>
|
64
|
+
"SELECT commits.commit_hash, COUNT(commit_files.file_id) AS files_changed
|
65
|
+
FROM commits
|
66
|
+
JOIN commit_files ON commits.commit_hash = commit_files.commit_hash
|
67
|
+
WHERE substr(commits.timestamp, 1, 4) = '2023'
|
68
|
+
GROUP BY commits.commit_hash
|
69
|
+
ORDER BY files_changed DESC
|
70
|
+
LIMIT 10;
|
71
|
+
",
|
72
|
+
"most_edited_files" =>
|
73
|
+
"SELECT files.file_path, COUNT(*) AS edit_count
|
74
|
+
FROM commit_files
|
75
|
+
JOIN files ON commit_files.file_id = files.file_id
|
76
|
+
JOIN commits ON commit_files.commit_hash = commits.commit_hash
|
77
|
+
WHERE substr(commits.timestamp, 1, 4) = '2023'
|
78
|
+
GROUP BY files.file_path
|
79
|
+
ORDER BY edit_count DESC
|
80
|
+
LIMIT 10;
|
81
|
+
;
|
82
|
+
",
|
83
|
+
}
|
84
|
+
|
85
|
+
queries.transform_values { |query| database.execute(query) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.emojis_for_titles
|
89
|
+
{
|
90
|
+
"top_contributors_of_the_year" => "🏆",
|
91
|
+
"commits_per_day_of_the_week" => "📅",
|
92
|
+
"most_active_months" => "📆",
|
93
|
+
"commits_with_most_significant_number_of_changes" => "📈",
|
94
|
+
"most_edited_files" => "📝",
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
data/package.json
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
{
|
2
|
+
"name": "pg-osc",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"main": "index.js",
|
5
|
+
"repository": "git@github.com:shayonj/pg-osc.git",
|
6
|
+
"author": "Shayon Mukherjee <shayonj@gmail.com>",
|
7
|
+
"license": "MIT",
|
8
|
+
"private": true,
|
9
|
+
"dependencies": {
|
10
|
+
"@prettier/plugin-ruby": "^3.2.2",
|
11
|
+
"prettier": "^2.8.8"
|
12
|
+
}
|
13
|
+
}
|