gitgo 0.3.3
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.
- data/History +44 -0
- data/License.txt +22 -0
- data/README +45 -0
- data/bin/gitgo +4 -0
- data/lib/gitgo.rb +1 -0
- data/lib/gitgo/app.rb +63 -0
- data/lib/gitgo/controller.rb +89 -0
- data/lib/gitgo/controllers/code.rb +198 -0
- data/lib/gitgo/controllers/issue.rb +76 -0
- data/lib/gitgo/controllers/repo.rb +186 -0
- data/lib/gitgo/controllers/wiki.rb +19 -0
- data/lib/gitgo/document.rb +680 -0
- data/lib/gitgo/document/invalid_document_error.rb +34 -0
- data/lib/gitgo/documents/comment.rb +20 -0
- data/lib/gitgo/documents/issue.rb +56 -0
- data/lib/gitgo/git.rb +941 -0
- data/lib/gitgo/git/tree.rb +315 -0
- data/lib/gitgo/git/utils.rb +59 -0
- data/lib/gitgo/helper.rb +3 -0
- data/lib/gitgo/helper/doc.rb +28 -0
- data/lib/gitgo/helper/form.rb +88 -0
- data/lib/gitgo/helper/format.rb +200 -0
- data/lib/gitgo/helper/html.rb +19 -0
- data/lib/gitgo/helper/utils.rb +85 -0
- data/lib/gitgo/index.rb +421 -0
- data/lib/gitgo/index/idx_file.rb +119 -0
- data/lib/gitgo/index/sha_file.rb +135 -0
- data/lib/gitgo/patches/grit.rb +47 -0
- data/lib/gitgo/repo.rb +626 -0
- data/lib/gitgo/repo/graph.rb +333 -0
- data/lib/gitgo/repo/node.rb +122 -0
- data/lib/gitgo/rest.rb +87 -0
- data/lib/gitgo/server.rb +114 -0
- data/lib/gitgo/version.rb +8 -0
- data/public/css/gitgo.css +24 -0
- data/public/javascript/gitgo.js +148 -0
- data/public/javascript/jquery-1.4.2.min.js +154 -0
- data/views/app/index.erb +4 -0
- data/views/app/timeline.erb +27 -0
- data/views/app/welcome.erb +13 -0
- data/views/code/_comment.erb +10 -0
- data/views/code/_comment_form.erb +14 -0
- data/views/code/_comments.erb +5 -0
- data/views/code/_commit.erb +25 -0
- data/views/code/_grepnav.erb +5 -0
- data/views/code/_treenav.erb +3 -0
- data/views/code/blob.erb +6 -0
- data/views/code/commit_grep.erb +35 -0
- data/views/code/commits.erb +11 -0
- data/views/code/diff.erb +10 -0
- data/views/code/grep.erb +32 -0
- data/views/code/index.erb +17 -0
- data/views/code/obj/blob.erb +4 -0
- data/views/code/obj/commit.erb +25 -0
- data/views/code/obj/tag.erb +25 -0
- data/views/code/obj/tree.erb +9 -0
- data/views/code/tree.erb +9 -0
- data/views/error.erb +19 -0
- data/views/issue/_issue.erb +15 -0
- data/views/issue/_issue_form.erb +39 -0
- data/views/issue/edit.erb +11 -0
- data/views/issue/index.erb +28 -0
- data/views/issue/new.erb +5 -0
- data/views/issue/show.erb +27 -0
- data/views/layout.erb +34 -0
- data/views/not_found.erb +1 -0
- data/views/repo/fsck.erb +29 -0
- data/views/repo/help.textile +5 -0
- data/views/repo/help/faq.textile +19 -0
- data/views/repo/help/howto.textile +31 -0
- data/views/repo/help/trouble.textile +28 -0
- data/views/repo/idx.erb +29 -0
- data/views/repo/index.erb +72 -0
- data/views/repo/status.erb +16 -0
- data/views/wiki/index.erb +3 -0
- metadata +253 -0
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Gitgo
|
4
|
+
class Index
|
5
|
+
|
6
|
+
# IdxFile is a wrapper providing access to a file of L packed integers.
|
7
|
+
class IdxFile
|
8
|
+
class << self
|
9
|
+
|
10
|
+
# Opens and returns an idx file in the specified mode. If a block is
|
11
|
+
# given the file is yielded to it and closed afterwards; in this case
|
12
|
+
# the return of open is the block result.
|
13
|
+
def open(path, mode="r")
|
14
|
+
idx_file = new(File.open(path, mode))
|
15
|
+
|
16
|
+
return idx_file unless block_given?
|
17
|
+
|
18
|
+
begin
|
19
|
+
yield(idx_file)
|
20
|
+
ensure
|
21
|
+
idx_file.close
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Reads the file and returns an array of integers.
|
26
|
+
def read(path)
|
27
|
+
open(path) {|idx_file| idx_file.read(nil) }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Opens the file and writes the integer; previous contents are
|
31
|
+
# replaced. Provide an array of integers to write multiple integers
|
32
|
+
# at once.
|
33
|
+
def write(path, int)
|
34
|
+
dir = File.dirname(path)
|
35
|
+
FileUtils.mkdir_p(dir) unless File.exists?(dir)
|
36
|
+
open(path, "w") {|idx_file| idx_file.write(int) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Opens the file and appends the int. Provide an array of integers to
|
40
|
+
# append multiple integers at once.
|
41
|
+
def append(path, int)
|
42
|
+
dir = File.dirname(path)
|
43
|
+
FileUtils.mkdir_p(dir) unless File.exists?(dir)
|
44
|
+
open(path, "a") {|idx_file| idx_file.write(int) }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Opens the file and removes the integers.
|
48
|
+
def rm(path, *ints)
|
49
|
+
return unless File.exists?(path)
|
50
|
+
|
51
|
+
current = read(path)
|
52
|
+
write(path, (current-ints))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# The pack format
|
57
|
+
PACK = "L*"
|
58
|
+
|
59
|
+
# The unpack format
|
60
|
+
UNPACK = "L*"
|
61
|
+
|
62
|
+
# The size of a packed integer
|
63
|
+
PACKED_ENTRY_SIZE = 4
|
64
|
+
|
65
|
+
# The file being wrapped
|
66
|
+
attr_reader :file
|
67
|
+
|
68
|
+
# Initializes a new ShaFile with the specified file. The file will be
|
69
|
+
# set to binary mode.
|
70
|
+
def initialize(file)
|
71
|
+
file.binmode
|
72
|
+
@file = file
|
73
|
+
end
|
74
|
+
|
75
|
+
# Closes file
|
76
|
+
def close
|
77
|
+
file.close
|
78
|
+
end
|
79
|
+
|
80
|
+
# The index of the current entry.
|
81
|
+
def current
|
82
|
+
file.pos / PACKED_ENTRY_SIZE
|
83
|
+
end
|
84
|
+
|
85
|
+
# Reads n entries from the start index and returns them as an array. Nil
|
86
|
+
# n will read all remaining entries and nil start will read from the
|
87
|
+
# current index.
|
88
|
+
def read(n=10, start=0)
|
89
|
+
if start
|
90
|
+
start_pos = start * PACKED_ENTRY_SIZE
|
91
|
+
file.pos = start_pos
|
92
|
+
end
|
93
|
+
|
94
|
+
str = file.read(n.nil? ? nil : n * PACKED_ENTRY_SIZE).to_s
|
95
|
+
unless str.length % PACKED_ENTRY_SIZE == 0
|
96
|
+
raise "invalid packed int length: #{str.length}"
|
97
|
+
end
|
98
|
+
|
99
|
+
str.unpack(UNPACK)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Writes the integers to the file at the current index. Provide an
|
103
|
+
# array of integers to write multiple integers at once.
|
104
|
+
def write(int)
|
105
|
+
int = [int] unless int.respond_to?(:pack)
|
106
|
+
file.write int.pack(PACK)
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
# Appends the integers to the file. Provide an array of integers to
|
111
|
+
# append multiple integers at once.
|
112
|
+
def append(int)
|
113
|
+
file.pos = file.size
|
114
|
+
write(int)
|
115
|
+
self
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Gitgo
|
4
|
+
class Index
|
5
|
+
|
6
|
+
# ShaFile is a wrapper providing access to a file of H packed shas.
|
7
|
+
class ShaFile
|
8
|
+
class << self
|
9
|
+
|
10
|
+
# Opens and returns an sha file in the specified mode. If a block is
|
11
|
+
# given the file is yielded to it and closed afterwards; in this case
|
12
|
+
# the return of open is the block result.
|
13
|
+
def open(path, mode="r")
|
14
|
+
sha_file = new(File.open(path, mode))
|
15
|
+
|
16
|
+
return sha_file unless block_given?
|
17
|
+
|
18
|
+
begin
|
19
|
+
yield(sha_file)
|
20
|
+
ensure
|
21
|
+
sha_file.close
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Reads the file and returns an array of shas.
|
26
|
+
def read(path)
|
27
|
+
open(path) {|sha_file| sha_file.read(nil) }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Opens the file and writes the sha; previous contents are replaced.
|
31
|
+
# Multiple shas may be written at once by providing a string of
|
32
|
+
# concatenated shas.
|
33
|
+
def write(path, sha)
|
34
|
+
dir = File.dirname(path)
|
35
|
+
FileUtils.mkdir_p(dir) unless File.exists?(dir)
|
36
|
+
open(path, "w") {|sha_file| sha_file.write(sha) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Opens the file and appends the sha. Multiple shas may be appended
|
40
|
+
# at once by providing a string of concatenated shas.
|
41
|
+
def append(path, sha)
|
42
|
+
dir = File.dirname(path)
|
43
|
+
FileUtils.mkdir_p(dir) unless File.exists?(dir)
|
44
|
+
open(path, "a") {|sha_file| sha_file.write(sha) }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Opens the file and removes the shas.
|
48
|
+
def rm(path, *shas)
|
49
|
+
return unless File.exists?(path)
|
50
|
+
|
51
|
+
current = read(path)
|
52
|
+
write(path, (current-shas).join)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# The pack format, optimized for packing multiple shas
|
57
|
+
PACK = "H*"
|
58
|
+
|
59
|
+
# The unpacking format
|
60
|
+
UNPACK = "H40"
|
61
|
+
|
62
|
+
# The size of an unpacked sha
|
63
|
+
ENTRY_SIZE = 40
|
64
|
+
|
65
|
+
# The size of a packed sha
|
66
|
+
PACKED_ENTRY_SIZE = 20
|
67
|
+
|
68
|
+
# The file being wrapped
|
69
|
+
attr_reader :file
|
70
|
+
|
71
|
+
# Initializes a new ShaFile with the specified file. The file will be
|
72
|
+
# set to binary mode.
|
73
|
+
def initialize(file)
|
74
|
+
file.binmode
|
75
|
+
@file = file
|
76
|
+
end
|
77
|
+
|
78
|
+
# Closes file
|
79
|
+
def close
|
80
|
+
file.close
|
81
|
+
end
|
82
|
+
|
83
|
+
# The index of the current entry.
|
84
|
+
def current
|
85
|
+
file.pos / PACKED_ENTRY_SIZE
|
86
|
+
end
|
87
|
+
|
88
|
+
# Reads n entries from the start index and returns them as an array. Nil
|
89
|
+
# n will read all remaining entries and nil start will read from the
|
90
|
+
# current index.
|
91
|
+
def read(n=10, start=0)
|
92
|
+
if start
|
93
|
+
start_pos = start * PACKED_ENTRY_SIZE
|
94
|
+
file.pos = start_pos
|
95
|
+
end
|
96
|
+
|
97
|
+
str = file.read(n.nil? ? nil : n * PACKED_ENTRY_SIZE).to_s
|
98
|
+
unless str.length % PACKED_ENTRY_SIZE == 0
|
99
|
+
raise "invalid packed sha length: #{str.length}"
|
100
|
+
end
|
101
|
+
entries = str.unpack(UNPACK * (str.length / PACKED_ENTRY_SIZE))
|
102
|
+
|
103
|
+
# clear out all missing entries, which will be empty
|
104
|
+
while last = entries.last
|
105
|
+
if last.empty?
|
106
|
+
entries.pop
|
107
|
+
else
|
108
|
+
break
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
entries
|
113
|
+
end
|
114
|
+
|
115
|
+
# Writes the sha to the file at the current index. Multiple shas may be
|
116
|
+
# written at once by providing a string of concatenated shas.
|
117
|
+
def write(sha)
|
118
|
+
unless sha.length % ENTRY_SIZE == 0
|
119
|
+
raise "invalid sha length: #{sha.length}"
|
120
|
+
end
|
121
|
+
|
122
|
+
file.write [sha].pack(PACK)
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
# Appends the sha to the file. Multiple shas may be appended at once by
|
127
|
+
# providing a string of concatenated shas.
|
128
|
+
def append(sha)
|
129
|
+
file.pos = file.size
|
130
|
+
write(sha)
|
131
|
+
self
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Gitgo
|
2
|
+
module Patches
|
3
|
+
class Grit::Actor
|
4
|
+
def <=>(another)
|
5
|
+
name <=> another.name
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Grit::Commit
|
10
|
+
|
11
|
+
# This patch allows file add/remove to be detected in diffs. For some
|
12
|
+
# reason with the original version (commented out) the diff is missing
|
13
|
+
# certain crucial lines in the output:
|
14
|
+
#
|
15
|
+
# diff --git a/alpha.txt b/alpha.txt
|
16
|
+
# index 0000000000000000000000000000000000000000..15db91c38a4cd47235961faa407304bf47ea5d15 100644
|
17
|
+
# --- a/alpha.txt
|
18
|
+
# +++ b/alpha.txt
|
19
|
+
# @@ -1 +1,2 @@
|
20
|
+
# +Contents of file alpha.
|
21
|
+
#
|
22
|
+
# vs
|
23
|
+
#
|
24
|
+
# diff --git a/alpha.txt b/alpha.txt
|
25
|
+
# new file mode 100644
|
26
|
+
# index 0000000000000000000000000000000000000000..15db91c38a4cd47235961faa407304bf47ea5d15
|
27
|
+
# --- /dev/null
|
28
|
+
# +++ b/alpha.txt
|
29
|
+
# @@ -0,0 +1 @@
|
30
|
+
# +Contents of file alpha.
|
31
|
+
#
|
32
|
+
# Perhaps the original drops into the pure-ruby version of git?
|
33
|
+
def self.diff(repo, a, b = nil, paths = [])
|
34
|
+
if b.is_a?(Array)
|
35
|
+
paths = b
|
36
|
+
b = nil
|
37
|
+
end
|
38
|
+
paths.unshift("--") unless paths.empty?
|
39
|
+
paths.unshift(b) unless b.nil?
|
40
|
+
paths.unshift(a)
|
41
|
+
# text = repo.git.diff({:full_index => true}, *paths)
|
42
|
+
text = repo.git.run('', :diff, '', {:full_index => true}, paths)
|
43
|
+
Grit::Diff.list_from_string(repo, text)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/gitgo/repo.rb
ADDED
@@ -0,0 +1,626 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'gitgo/git'
|
3
|
+
require 'gitgo/index'
|
4
|
+
require 'gitgo/repo/graph'
|
5
|
+
|
6
|
+
module Gitgo
|
7
|
+
# Repo represents the internal data store used by Gitgo. Repos consist of a
|
8
|
+
# Git instance for storing documents in the repository, and an Index
|
9
|
+
# instance for queries on the documents. The internal workings of Repo are a
|
10
|
+
# bit complex; this document provides terminology and details on how
|
11
|
+
# documents and associations are stored. See Index and Graph for how
|
12
|
+
# document information is accessed.
|
13
|
+
#
|
14
|
+
# == Terminology
|
15
|
+
#
|
16
|
+
# Gitgo documents are hashes of attributes that can be serialized as JSON.
|
17
|
+
# The Gitgo::Document model adds structure to these hashes and enforces data
|
18
|
+
# validity, but insofar as Repo is concerned, a document is a serializable
|
19
|
+
# hash. Documents are linked into a document graph -- a directed acyclic
|
20
|
+
# graph (DAG) of document nodes that represent, for example, a chain of
|
21
|
+
# comments making a conversation. A given repo can be thought of as storing
|
22
|
+
# multiple DAGs, each made up of multiple documents.
|
23
|
+
#
|
24
|
+
# The DAGs used by Gitgo are a little weird because they use some nodes to
|
25
|
+
# represent revisions and other nodes to represent the 'current' nodes in a
|
26
|
+
# graph (this setup allows documents to be immutable, and thereby to prevent
|
27
|
+
# merge conflicts).
|
28
|
+
#
|
29
|
+
# Normally a DAG has this conceptual structure:
|
30
|
+
#
|
31
|
+
# head
|
32
|
+
# |
|
33
|
+
# parent
|
34
|
+
# |
|
35
|
+
# node
|
36
|
+
# |
|
37
|
+
# child
|
38
|
+
# |
|
39
|
+
# tail
|
40
|
+
#
|
41
|
+
# By contrast, the DAGs used by Gitgo are structured like this:
|
42
|
+
#
|
43
|
+
# head
|
44
|
+
# |
|
45
|
+
# parent
|
46
|
+
# |
|
47
|
+
# original -> previous -> node -> update -> current version
|
48
|
+
# |
|
49
|
+
# child
|
50
|
+
# |
|
51
|
+
# tail
|
52
|
+
#
|
53
|
+
# The extra dimension of updates may be unwound to replace all previous
|
54
|
+
# versions of a node with the current version(s), so for example:
|
55
|
+
#
|
56
|
+
# a a
|
57
|
+
# | |
|
58
|
+
# b -> b' becomes b'
|
59
|
+
# | |
|
60
|
+
# c c
|
61
|
+
#
|
62
|
+
# The full DAG is refered to as the 'convoluted graph' and the current DAG
|
63
|
+
# is the 'deconvoluted graph'. The logic performing the deconvolution is
|
64
|
+
# encapsulated in Graph and Node.
|
65
|
+
#
|
66
|
+
# Parent-child associations are referred to as links, while previous-update
|
67
|
+
# associations are referred to as updates. Links and updates are
|
68
|
+
# collectively referred to as associations.
|
69
|
+
#
|
70
|
+
# There are two additional types of associations; create and delete. Create
|
71
|
+
# associations occur when a sha is associated with the empty sha (ie the sha
|
72
|
+
# for an empty document). These associations place new documents along a
|
73
|
+
# path in the repo when the new document isn't a child or update. Deletes
|
74
|
+
# associate a sha with itself; these act as a break in the DAG such that all
|
75
|
+
# subsequent links and updates are omitted.
|
76
|
+
#
|
77
|
+
# The first member in an association (parent/previous/sha) is a source and
|
78
|
+
# the second (child/update/sha) is a target.
|
79
|
+
#
|
80
|
+
# == Storage
|
81
|
+
#
|
82
|
+
# Documents are stored on a dedicated git branch in a way that prevents
|
83
|
+
# merge conflicts and allows merges to directly add nodes anywhere in a
|
84
|
+
# document graph. The branch may be checked out and handled like any other
|
85
|
+
# git branch, although typically users manage the gitgo branch through Gitgo
|
86
|
+
# itself.
|
87
|
+
#
|
88
|
+
# Individual documents are stored with their associations along sha-based
|
89
|
+
# paths like 'so/urce/target' where the source is split into substrings of
|
90
|
+
# length 2 and 38. The mode and the relationship of the source-target shas
|
91
|
+
# determine the type of association involved. The logic breaks down like
|
92
|
+
# this ('-' refers to the empty sha, and a/b to different shas):
|
93
|
+
#
|
94
|
+
# source target mode type
|
95
|
+
# a - 644 create
|
96
|
+
# a b 644 link
|
97
|
+
# a b 640 update
|
98
|
+
# a a 644 delete
|
99
|
+
#
|
100
|
+
# Using this system, a traveral of the associations is enough to determine
|
101
|
+
# how documents are related in a graph without loading documents into
|
102
|
+
# memory.
|
103
|
+
#
|
104
|
+
# == Implementation Note
|
105
|
+
#
|
106
|
+
# Repo is organized around an env hash that represents the rack env for a
|
107
|
+
# particular request. Objects used by Repo are cached into env for re-use
|
108
|
+
# across multiple requests, when possible. The 'gitgo.*' constants are used
|
109
|
+
# to identify cached objects.
|
110
|
+
#
|
111
|
+
# Repo knows how to initialize all the objects it uses. An empty env or a
|
112
|
+
# partially filled env may be used to initialize a Repo.
|
113
|
+
#
|
114
|
+
class Repo
|
115
|
+
class << self
|
116
|
+
|
117
|
+
# Initializes a new Repo to the git repository at the specified path.
|
118
|
+
# Options are the same as for Git.init.
|
119
|
+
def init(path, options={})
|
120
|
+
git = Git.init(path, options)
|
121
|
+
new(GIT => git)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Sets env as the thread-specific env and returns the currently set env.
|
125
|
+
def set_env(env)
|
126
|
+
current = Thread.current[ENVIRONMENT]
|
127
|
+
Thread.current[ENVIRONMENT] = env
|
128
|
+
current
|
129
|
+
end
|
130
|
+
|
131
|
+
# Sets env for the block.
|
132
|
+
def with_env(env)
|
133
|
+
begin
|
134
|
+
current = set_env(env)
|
135
|
+
yield
|
136
|
+
ensure
|
137
|
+
set_env(current)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# The thread-specific env currently in scope (see set_env). The env
|
142
|
+
# stores all the objects used by a Repo and typically represents the
|
143
|
+
# rack-env for a specific server request.
|
144
|
+
#
|
145
|
+
# Raises an error if no env is in scope.
|
146
|
+
def env
|
147
|
+
Thread.current[ENVIRONMENT] or raise("no env in scope")
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns the current Repo, ie env[REPO]. Initializes and caches a new
|
151
|
+
# Repo in env if env[REPO] is not set.
|
152
|
+
def current
|
153
|
+
env[REPO] ||= new(env)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
ENVIRONMENT = 'gitgo.env'
|
158
|
+
PATH = 'gitgo.path'
|
159
|
+
OPTIONS = 'gitgo.options'
|
160
|
+
GIT = 'gitgo.git'
|
161
|
+
INDEX = 'gitgo.index'
|
162
|
+
REPO = 'gitgo.repo'
|
163
|
+
CACHE = 'gitgo.cache'
|
164
|
+
|
165
|
+
# Matches a path -- 'ab/xyz/sha'. After the match:
|
166
|
+
#
|
167
|
+
# $1:: ab
|
168
|
+
# $2:: xyz
|
169
|
+
# $3:: sha
|
170
|
+
#
|
171
|
+
DOCUMENT_PATH = /^(.{2})\/(.{38})\/(.{40})$/
|
172
|
+
|
173
|
+
# The default blob mode used for added blobs
|
174
|
+
DEFAULT_MODE = '100644'.to_sym
|
175
|
+
|
176
|
+
# The blob mode used to identify updates
|
177
|
+
UPDATE_MODE = '100640'.to_sym
|
178
|
+
|
179
|
+
FILE = 'gitgo'
|
180
|
+
|
181
|
+
# The repo env, typically the same as a request env.
|
182
|
+
attr_reader :env
|
183
|
+
|
184
|
+
# Initializes a new Repo with the specified env.
|
185
|
+
def initialize(env={})
|
186
|
+
@env = env
|
187
|
+
end
|
188
|
+
|
189
|
+
def head
|
190
|
+
git.head
|
191
|
+
end
|
192
|
+
|
193
|
+
def branch
|
194
|
+
git.branch
|
195
|
+
end
|
196
|
+
|
197
|
+
def upstream_branch
|
198
|
+
git.upstream_branch
|
199
|
+
end
|
200
|
+
|
201
|
+
def resolve(sha)
|
202
|
+
git.resolve(sha) rescue sha
|
203
|
+
end
|
204
|
+
|
205
|
+
# Returns the path to git repository. Path is determined from env[PATH],
|
206
|
+
# or inferred and set in env from env[GIT]. The default path is Dir.pwd.
|
207
|
+
def path
|
208
|
+
env[PATH] ||= (env.has_key?(GIT) ? env[GIT].path : Dir.pwd)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns the Git instance set in env[GIT]. If no instance is set then
|
212
|
+
# one will be initialized using env[PATH] and env[OPTIONS].
|
213
|
+
#
|
214
|
+
# Note that given the chain of defaults, git will be initialized to
|
215
|
+
# Dir.pwd if the env has no PATH or GIT set.
|
216
|
+
def git
|
217
|
+
env[GIT] ||= Git.init(path, env[OPTIONS] || {})
|
218
|
+
end
|
219
|
+
|
220
|
+
# Returns the Index instance set in env[INDEX]. If no instance is set then
|
221
|
+
# one will be initialized under the git working directory, specific to the
|
222
|
+
# git branch. For instance:
|
223
|
+
#
|
224
|
+
# .git/gitgo/refs/branch/index
|
225
|
+
#
|
226
|
+
def index
|
227
|
+
env[INDEX] ||= Index.new(File.join(git.work_dir, 'refs', git.branch, 'index'))
|
228
|
+
end
|
229
|
+
|
230
|
+
# Returns or initializes a self-populating cache of attribute hashes in
|
231
|
+
# env[CACHE]. Attribute hashes are are keyed by sha.
|
232
|
+
def cache
|
233
|
+
env[CACHE] ||= Hash.new {|hash, sha| hash[sha] = read(sha) }
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns the cached attrs hash for the specified sha, or nil.
|
237
|
+
def [](sha)
|
238
|
+
cache[sha]
|
239
|
+
end
|
240
|
+
|
241
|
+
# Sets the cached attrs for the specified sha.
|
242
|
+
def []=(sha, attrs)
|
243
|
+
cache[sha] = attrs
|
244
|
+
end
|
245
|
+
|
246
|
+
# Returns the sha for an empty string, and ensures the corresponding
|
247
|
+
# object is set in the repo.
|
248
|
+
def empty_sha
|
249
|
+
@empty_sha ||= git.set(:blob, '')
|
250
|
+
end
|
251
|
+
|
252
|
+
# Creates a nested sha path like: ab/xyz/paths
|
253
|
+
def sha_path(sha, *paths)
|
254
|
+
paths.unshift sha[2,38]
|
255
|
+
paths.unshift sha[0,2]
|
256
|
+
paths
|
257
|
+
end
|
258
|
+
|
259
|
+
# Returns true if the given commit has an empty 'gitgo' file in it's tree.
|
260
|
+
def branch?(sha)
|
261
|
+
return false if sha.nil?
|
262
|
+
return false unless sha = resolve(sha)
|
263
|
+
return false unless commit = git.get(:commit, sha)
|
264
|
+
|
265
|
+
blob = commit.tree/FILE
|
266
|
+
blob && blob.data.empty? ? true : false
|
267
|
+
end
|
268
|
+
|
269
|
+
# Returns an array of refs representing gitgo branches.
|
270
|
+
def refs
|
271
|
+
select_branches(git.grit.refs)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Returns an array of remotes representing gitgo branches.
|
275
|
+
def remotes
|
276
|
+
select_branches(git.grit.remotes)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Serializes and sets the attributes as a blob in the git repo and caches
|
280
|
+
# the attributes by the blob sha. Returns the blob sha.
|
281
|
+
#
|
282
|
+
# Note that save does not put the blob along a path in the repo;
|
283
|
+
# immediately after save the blob is hanging and will be gc'ed by git
|
284
|
+
# unless set into a path by create, link, or update.
|
285
|
+
def save(attrs)
|
286
|
+
sha = git.set(:blob, JSON.generate(attrs))
|
287
|
+
cache[sha] = attrs
|
288
|
+
sha
|
289
|
+
end
|
290
|
+
|
291
|
+
# Creates a create association for the sha:
|
292
|
+
#
|
293
|
+
# sh/a/empty_sha (DEFAULT_MODE, sha)
|
294
|
+
#
|
295
|
+
def create(sha)
|
296
|
+
git[sha_path(sha, empty_sha)] = [DEFAULT_MODE, sha]
|
297
|
+
sha
|
298
|
+
end
|
299
|
+
|
300
|
+
# Reads and deserializes the specified hash of attrs. If sha does not
|
301
|
+
# indicate a blob that deserializes as JSON then read returns nil.
|
302
|
+
def read(sha)
|
303
|
+
begin
|
304
|
+
JSON.parse(git.get(:blob, sha).data)
|
305
|
+
rescue JSON::ParserError, Errno::EISDIR
|
306
|
+
nil
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Creates a link association for parent and child:
|
311
|
+
#
|
312
|
+
# pa/rent/child (DEFAULT_MODE, child)
|
313
|
+
#
|
314
|
+
def link(parent, child)
|
315
|
+
git[sha_path(parent, child)] = [DEFAULT_MODE, child]
|
316
|
+
self
|
317
|
+
end
|
318
|
+
|
319
|
+
# Creates an update association for old and new shas:
|
320
|
+
#
|
321
|
+
# ol/d_sha/new_sha (UPDATE_MODE, new_sha)
|
322
|
+
#
|
323
|
+
def update(old_sha, new_sha)
|
324
|
+
git[sha_path(old_sha, new_sha)] = [UPDATE_MODE, new_sha]
|
325
|
+
self
|
326
|
+
end
|
327
|
+
|
328
|
+
# Creates a delete association for the sha:
|
329
|
+
#
|
330
|
+
# sh/a/sha (DEFAULT_MODE, empty_sha)
|
331
|
+
#
|
332
|
+
def delete(sha)
|
333
|
+
git[sha_path(sha, sha)] = [DEFAULT_MODE, empty_sha]
|
334
|
+
self
|
335
|
+
end
|
336
|
+
|
337
|
+
# Returns the operative sha in an association, ie the source in a
|
338
|
+
# head/delete association and the target in a link/update association.
|
339
|
+
def assoc_sha(source, target)
|
340
|
+
case target
|
341
|
+
when source then source
|
342
|
+
when empty_sha then source
|
343
|
+
else target
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Returns the mode of the specified association.
|
348
|
+
def assoc_mode(source, target)
|
349
|
+
tree = git.tree.subtree(sha_path(source))
|
350
|
+
return nil unless tree
|
351
|
+
|
352
|
+
mode, sha = tree[target]
|
353
|
+
mode
|
354
|
+
end
|
355
|
+
|
356
|
+
# Returns the association type given the source, target, and mode.
|
357
|
+
def assoc_type(source, target, mode=assoc_mode(source, target))
|
358
|
+
case mode
|
359
|
+
when DEFAULT_MODE
|
360
|
+
case target
|
361
|
+
when empty_sha then :create
|
362
|
+
when source then :delete
|
363
|
+
else :link
|
364
|
+
end
|
365
|
+
when UPDATE_MODE
|
366
|
+
:update
|
367
|
+
else
|
368
|
+
:invalid
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Returns a hash of associations for the source, mainly used as a
|
373
|
+
# convenience method during testing.
|
374
|
+
def associations(source, sort=true)
|
375
|
+
associations = {}
|
376
|
+
links = []
|
377
|
+
updates = []
|
378
|
+
|
379
|
+
each_assoc(source) do |sha, type|
|
380
|
+
case type
|
381
|
+
when :create, :delete
|
382
|
+
associations[type] = true
|
383
|
+
when :link
|
384
|
+
links << sha
|
385
|
+
when :update
|
386
|
+
updates << sha
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
unless links.empty?
|
391
|
+
|
392
|
+
associations[:links] = links
|
393
|
+
end
|
394
|
+
|
395
|
+
unless updates.empty?
|
396
|
+
updates.sort! if sort
|
397
|
+
associations[:updates] = updates
|
398
|
+
end
|
399
|
+
|
400
|
+
associations
|
401
|
+
end
|
402
|
+
|
403
|
+
# Yield each association for source to the block, with the association sha
|
404
|
+
# and type. Returns self.
|
405
|
+
def each_assoc(source) # :yields: sha, type
|
406
|
+
return self if source.nil?
|
407
|
+
|
408
|
+
target_tree = git.tree.subtree(sha_path(source))
|
409
|
+
target_tree.each_pair do |target, (mode, sha)|
|
410
|
+
yield assoc_sha(source, target), assoc_type(source, target, mode)
|
411
|
+
end if target_tree
|
412
|
+
|
413
|
+
self
|
414
|
+
end
|
415
|
+
|
416
|
+
# Yields the sha of each document in the repo, in no particular order and
|
417
|
+
# with duplicates for every link/update that has multiple association
|
418
|
+
# sources.
|
419
|
+
def each
|
420
|
+
git.tree.each_pair(true) do |ab, xyz_tree|
|
421
|
+
next unless ab.length == 2
|
422
|
+
|
423
|
+
xyz_tree.each_pair(true) do |xyz, target_tree|
|
424
|
+
source = "#{ab}#{xyz}"
|
425
|
+
|
426
|
+
target_tree.keys.each do |target|
|
427
|
+
doc_sha = assoc_sha(source, target)
|
428
|
+
yield(doc_sha) if doc_sha
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Initializes a Graph for the sha.
|
435
|
+
def graph(sha)
|
436
|
+
Graph.new(self, sha)
|
437
|
+
end
|
438
|
+
|
439
|
+
# Returns an array of shas representing recent documents added.
|
440
|
+
def timeline(options={})
|
441
|
+
options = {:n => 10, :offset => 0}.merge(options)
|
442
|
+
offset = options[:offset]
|
443
|
+
n = options[:n]
|
444
|
+
|
445
|
+
shas = []
|
446
|
+
return shas if n <= 0
|
447
|
+
|
448
|
+
dates = index.values('date').sort.reverse
|
449
|
+
index.each_sha('date', dates) do |sha|
|
450
|
+
if block_given?
|
451
|
+
next unless yield(sha)
|
452
|
+
end
|
453
|
+
|
454
|
+
if offset > 0
|
455
|
+
offset -= 1
|
456
|
+
else
|
457
|
+
shas << sha
|
458
|
+
break if n && shas.length == n
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
shas
|
463
|
+
end
|
464
|
+
|
465
|
+
# Returns an array of revisions (commits) reachable from the sha. These
|
466
|
+
# revisions are cached for quick retreival.
|
467
|
+
def rev_list(sha)
|
468
|
+
sha = sha.to_sym
|
469
|
+
unless cache.has_key?(sha)
|
470
|
+
cache[sha] = git.rev_list(sha.to_s)
|
471
|
+
end
|
472
|
+
|
473
|
+
cache[sha]
|
474
|
+
end
|
475
|
+
|
476
|
+
# Returns a list of document shas that have been added ('A') between a and
|
477
|
+
# b. Deleted ('D') or modified ('M') documents can be specified using
|
478
|
+
# type.
|
479
|
+
def diff(a, b, type='A')
|
480
|
+
if a == b || b.nil?
|
481
|
+
return []
|
482
|
+
end
|
483
|
+
|
484
|
+
paths = a.nil? ? git.ls_tree(b) : git.diff_tree(a, b)[type]
|
485
|
+
paths.collect! do |path|
|
486
|
+
ab, xyz, target = path.split('/', 3)
|
487
|
+
assoc_sha("#{ab}#{xyz}", target)
|
488
|
+
end
|
489
|
+
|
490
|
+
paths.compact!
|
491
|
+
paths
|
492
|
+
end
|
493
|
+
|
494
|
+
# Generates a status message based on currently uncommitted changes.
|
495
|
+
def status
|
496
|
+
unless block_given?
|
497
|
+
return status {|sha| sha}
|
498
|
+
end
|
499
|
+
|
500
|
+
lines = []
|
501
|
+
git.status.each_pair do |path, state|
|
502
|
+
ab, xyz, target = path.split('/', 3)
|
503
|
+
source = "#{ab}#{xyz}"
|
504
|
+
|
505
|
+
sha = assoc_sha(source, target)
|
506
|
+
type = assoc_type(source, target)
|
507
|
+
|
508
|
+
status = case assoc_type(source, target)
|
509
|
+
when :create
|
510
|
+
type = self[sha]['type']
|
511
|
+
[type || 'doc', yield(sha)]
|
512
|
+
when :link
|
513
|
+
['link', "#{yield(source)} to #{yield(target)}"]
|
514
|
+
when :update
|
515
|
+
['update', "#{yield(target)} was #{yield(source)}"]
|
516
|
+
when :delete
|
517
|
+
['delete', yield(sha)]
|
518
|
+
else
|
519
|
+
['unknown', path]
|
520
|
+
end
|
521
|
+
|
522
|
+
if status
|
523
|
+
status.unshift state_str(state)
|
524
|
+
lines << status
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
indent = lines.collect {|(state, type, msg)| type.length }.max
|
529
|
+
format = "%s %-#{indent}s %s"
|
530
|
+
lines.collect! {|ary| format % ary }
|
531
|
+
lines.sort!
|
532
|
+
lines.join("\n")
|
533
|
+
end
|
534
|
+
|
535
|
+
# Commits any changes to git and writes the index to disk. The commit
|
536
|
+
# message is inferred from the status, if left unspecified. Commit will
|
537
|
+
# raise an error if there are no changes to commit.
|
538
|
+
def commit(msg=status)
|
539
|
+
setup unless head
|
540
|
+
|
541
|
+
sha = git.commit(msg)
|
542
|
+
index.write(sha)
|
543
|
+
sha
|
544
|
+
end
|
545
|
+
|
546
|
+
# Same as commit but does not check if there are changes to commit, useful
|
547
|
+
# when you know there are changes to commit and don't want the overhead of
|
548
|
+
# checking for changes.
|
549
|
+
def commit!(msg=status)
|
550
|
+
setup unless head
|
551
|
+
|
552
|
+
sha = git.commit!(msg)
|
553
|
+
index.write(sha)
|
554
|
+
sha
|
555
|
+
end
|
556
|
+
|
557
|
+
def setup(upstream_branch=nil)
|
558
|
+
if head
|
559
|
+
raise "already setup on: #{branch} (#{head})"
|
560
|
+
end
|
561
|
+
|
562
|
+
if upstream_branch.nil? || upstream_branch.empty?
|
563
|
+
tree = Git::Tree.new
|
564
|
+
tree[FILE] = [git.default_blob_mode, empty_sha]
|
565
|
+
mode, sha = tree.write_to(git)
|
566
|
+
git.commit!("setup gitgo", :tree => sha)
|
567
|
+
|
568
|
+
current_tree = git.tree
|
569
|
+
git.reset
|
570
|
+
git.tree.merge!(current_tree)
|
571
|
+
|
572
|
+
return self
|
573
|
+
end
|
574
|
+
|
575
|
+
unless branch?(upstream_branch)
|
576
|
+
raise "not a gitgo branch: #{upstream_branch.inspect}"
|
577
|
+
end
|
578
|
+
|
579
|
+
if git.tracking_branch?(upstream_branch)
|
580
|
+
git.track(upstream_branch)
|
581
|
+
end
|
582
|
+
git.merge(upstream_branch)
|
583
|
+
|
584
|
+
cache.clear
|
585
|
+
index.reset
|
586
|
+
self
|
587
|
+
end
|
588
|
+
|
589
|
+
def checkout(branch)
|
590
|
+
git.checkout(branch)
|
591
|
+
self
|
592
|
+
end
|
593
|
+
|
594
|
+
def reset(full=false)
|
595
|
+
git.reset(full)
|
596
|
+
cache.clear
|
597
|
+
index.reset
|
598
|
+
self
|
599
|
+
end
|
600
|
+
|
601
|
+
# Sets self as the current Repo for the duration of the block.
|
602
|
+
def scope
|
603
|
+
Repo.with_env(REPO => self) { yield }
|
604
|
+
end
|
605
|
+
|
606
|
+
protected
|
607
|
+
|
608
|
+
def select_branches(refs) # :nodoc:
|
609
|
+
results = {}
|
610
|
+
refs.select do |ref|
|
611
|
+
sha = ref.commit.id
|
612
|
+
results[sha] ||= (branch?(sha) ? ref : nil)
|
613
|
+
end
|
614
|
+
|
615
|
+
results.values.compact
|
616
|
+
end
|
617
|
+
|
618
|
+
def state_str(state) # :nodoc:
|
619
|
+
case state
|
620
|
+
when :add then '+'
|
621
|
+
when :rm then '-'
|
622
|
+
else '~'
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|
626
|
+
end
|