branch_base 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|