gitgo 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|