change_agent 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 637bed85fb1871ec16e91eccd2e654cacc3718b1
4
- data.tar.gz: ac8434e78322e91231c2b8d446df980eb8472a94
3
+ metadata.gz: 1b4df46f6edb351d190c8e7473c65ba64a593add
4
+ data.tar.gz: 51785477c0e84d66cd88cb989a6b4eab7d9477ae
5
5
  SHA512:
6
- metadata.gz: 6b12aa9d46caeefcd917f72bf9a2ca4f514dcb55044a5a559d24031c83e8747680879a1873578e1d5875faa31b4a19831726af3dc5f4f6de0dd6de287355252a
7
- data.tar.gz: 652be6634b377ccba3b04bcadeafae8dc314e5fa3ccced55377e3e53dc6dd46a4de6eb5c8c97a9ca448ab98912020d8404ec8bd89f2a3a807f9f71b70e403355
6
+ metadata.gz: 111998eab4ab45e1ea323903b33ff38195f5b0b93064575123242b75de8cee1ff66af72a91fcfb832243d2b54e855bcf43d470e906acbd0fed470bf45e730a7e
7
+ data.tar.gz: f09329d554f48f12bf7b7e1841b559a251ee74416cd35609a242f51ea57292861781984afaa2330b6f6698fe151c435ea4e8c53ac5ce3b7ec49b4f72b36b6d2b
data/.gitignore CHANGED
@@ -1,22 +1,8 @@
1
1
  *.gem
2
- *.rbc
3
2
  .bundle
4
3
  .config
5
- .yardoc
6
4
  Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
5
  test/tmp
16
- test/version_tmp
17
6
  tmp
18
7
  *.bundle
19
- *.so
20
- *.o
21
- *.a
22
- mkmf.log
8
+ .env
data/README.md CHANGED
@@ -12,6 +12,8 @@ But wait. What if I told you you could just commit each press release to Git, an
12
12
 
13
13
  Having built the first app more times then I'd like to admit, I thought I'd make a Gem to facilitate building lightweight apps that use Git to track changes to scraped documents (or whatever you want, really).
14
14
 
15
+ Want to see it in action? Check out [this lightweight demo](https://github.com/benbalter/change_agent_demo), scrapping the White House RSS feed.
16
+
15
17
  ## Okay, I'm sold. How do I use it?
16
18
 
17
19
  ChangeAgent writes values to the file system based on a given key, and immediately commits the file to Git, providing you with both a snapshot and a timestamp for every change.
@@ -31,7 +33,7 @@ change_agent.get "foo"
31
33
 
32
34
  ### Namespaced usage
33
35
 
34
- Keys (files) are intended to be namespaced when logically grouped. In the above example, if you were storing congressional press releases, you might store Rep. Balter's Nov 26th press release on puppies as "balter/2014/11/26/puppies.html", or just "balter/2014-11-26-puppies.txt" or even just "balter/puppies".
36
+ Keys (files) are intended to be namespaced when logically grouped. In the above example, if you were storing congressional press releases, you might store Rep. Balter's Nov 26th press release on puppies as `balter/2014/11/26/puppies.html`, or just `balter/2014-11-26-puppies.txt` or even just `balter/puppies`.
35
37
 
36
38
  ```ruby
37
39
  change_agent.set "foo/bar", "baz"
@@ -46,7 +48,7 @@ It's really up to you, but you'll get performance and usability bumps the more y
46
48
  ### Cloning an existing repo / datastore
47
49
 
48
50
  ```ruby
49
- repo = "https://github.com/benbalter/some_repo"
51
+ repo = "https://github.com/benbalter/change_agent_demo"
50
52
  directory = "data"
51
53
  change_agent = ChangeAgent::Client.new(directory, repo)
52
54
 
@@ -56,14 +58,32 @@ change_agent.get("foo")
56
58
 
57
59
  ### Pushing and pulling
58
60
 
59
- Ready to push your Git repo to a server? Assuming you've already got a remote set up, it's as simple as
61
+ Ready to push your Git repo to a server? It's as simple as:
60
62
 
61
63
  ```ruby
64
+ # add a remote (if there's not already one from the clone)
65
+ change_agent.add_remote "origin", "https://github.com/benbalter/change_agent_demo"
66
+
62
67
  # pull in the latest data
63
- change_agent.git.pull
68
+ change_agent.pull
64
69
 
65
70
  # push the data
66
- change_agent.git.push
71
+ change_agent.push
72
+
73
+ # do both
74
+ change_agent.sync
75
+ ```
76
+
77
+ ### Authentication
78
+
79
+ By default, Change Agent supports [token-bassed authentication](https://github.com/blog/1270-easier-builds-and-deployments-using-git-over-https-and-oauth). Simply pass an OAuth token via the `GITHUB_TOKEN` environmental variable and ensure all remotes use the `https` protocol. Change Agent will take care of the rest. You'll likely want to use a bot account for this.
80
+
81
+ Rugged supports additional authentication strategies (such as ssh key). For more information see [Rugged](https://github.com/libgit2/rugged/blob/master/lib/rugged/credentials.rb). Here's an example of how you might implement an alternative authentication mechanism:
82
+
83
+ ```ruby
84
+ change_agent = ChangeAgent::Client.new "data", "https://github.com/benbalter/change_agent_demo"
85
+ creds = Rugged::Credentials::UserPassword.new :username => "benbalter", :password => "passw0rd"
86
+ change_agent.credentials = creds
67
87
  ```
68
88
 
69
89
  ## Project status
data/change_agent.gemspec CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_dependency "rugged"
21
+ spec.add_dependency "dotenv"
21
22
  spec.add_development_dependency "bundler", "~> 1.6"
22
23
  spec.add_development_dependency "rake"
23
24
  spec.add_development_dependency "pry"
data/lib/change_agent.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require_relative "change_agent/version"
2
2
  require_relative "change_agent/document"
3
+ require_relative "change_agent/sync"
3
4
  require_relative "change_agent/client"
4
5
  require "rugged"
5
6
  require 'pathname'
7
+ require "dotenv"
6
8
 
7
9
  module ChangeAgent
8
10
 
@@ -11,3 +13,5 @@ module ChangeAgent
11
13
  end
12
14
 
13
15
  end
16
+
17
+ Dotenv.load
@@ -1,6 +1,7 @@
1
1
  module ChangeAgent
2
2
  class Client
3
3
 
4
+ include ChangeAgent::Sync
4
5
  attr_accessor :directory
5
6
 
6
7
  def initialize(directory=nil, remote=nil)
@@ -12,14 +13,15 @@ module ChangeAgent
12
13
  if @remote.nil?
13
14
  @repo ||= Rugged::Repository.init_at directory
14
15
  else
15
- @repo ||= Rugged::Repository.clone_at @remote, directory
16
+ @repo ||= Rugged::Repository.clone_at @remote, directory, {:credentials => credentials}
16
17
  end
17
18
  end
18
19
 
19
20
  def set(key, value)
20
21
  document = Document.new(key, self)
21
22
  document.contents = value
22
- document.write
23
+ return unless document.changed?
24
+ document.save
23
25
  document
24
26
  end
25
27
 
@@ -38,5 +40,6 @@ module ChangeAgent
38
40
  def inspect
39
41
  "#<ChangeAgent::Client repo=\"#{directory}\">"
40
42
  end
43
+
41
44
  end
42
45
  end
@@ -19,17 +19,14 @@ module ChangeAgent
19
19
  end
20
20
 
21
21
  def contents
22
- @contents ||= begin
23
- tree = repo.head.target.tree
24
- blob = repo.lookup tree.path(path)[:oid]
25
- blob.content
26
- end
27
- rescue Rugged::ReferenceError, Rugged::TreeError
28
- nil
22
+ @contents ||= blob_contents
29
23
  end
30
24
 
31
- def write
32
- clean_path
25
+ def changed?
26
+ contents != blob_contents
27
+ end
28
+
29
+ def save
33
30
  oid = repo.write contents, :blob
34
31
  repo.index.add(path: path, oid: oid, mode: 0100644)
35
32
 
@@ -39,6 +36,7 @@ module ChangeAgent
39
36
  tree: repo.index.write_tree(repo),
40
37
  update_ref: 'HEAD'
41
38
  end
39
+ alias_method :write, :save
42
40
 
43
41
  def delete(file=path)
44
42
  repo.index.remove(file)
@@ -48,6 +46,8 @@ module ChangeAgent
48
46
  parents: [repo.head.target],
49
47
  tree: repo.index.write_tree(repo),
50
48
  update_ref: 'HEAD'
49
+ rescue Rugged::IndexError
50
+ false
51
51
  end
52
52
 
53
53
  def inspect
@@ -56,15 +56,11 @@ module ChangeAgent
56
56
 
57
57
  private
58
58
 
59
- def clean_path
60
- return if repo.empty?
61
- dirs = []
59
+ def blob_contents
62
60
  tree = repo.head.target.tree
63
- path.split("/").each do |part|
64
- file = dirs.push(part).join("/")
65
- delete(file) if tree.path(file)
66
- end
67
- rescue Rugged::TreeError
61
+ blob = repo.lookup tree.path(path)[:oid]
62
+ blob.content
63
+ rescue Rugged::ReferenceError, Rugged::TreeError
68
64
  nil
69
65
  end
70
66
  end
@@ -0,0 +1,102 @@
1
+ module ChangeAgent
2
+ module Sync
3
+
4
+ class MergeConflict < StandardError; end
5
+ class MissingRemote < ArgumentError; end
6
+
7
+ attr_writer :credentials
8
+
9
+ DEFAULT_REMOTE = "origin"
10
+ DEFAULT_REMOTE_BRANCH = "origin/master"
11
+ DEFAULT_LOCAL_REF = "refs/heads/master"
12
+
13
+ # Default to token-based credentials passed as GITHUB_TOKEN
14
+ # Can be over ridden by overwritting @credentials with a
15
+ # different Rugged Credentialing method
16
+ def credentials
17
+ @credentials ||= Rugged::Credentials::UserPassword.new({
18
+ :username => "x-oauth-basic",
19
+ :password => ENV["GITHUB_TOKEN"]
20
+ })
21
+ end
22
+
23
+ # Helper method to return all remots
24
+ def remotes
25
+ repo.remotes
26
+ end
27
+
28
+ # Helper method to simplify adding a remote
29
+ def add_remote(name, url)
30
+ remotes.create name, url
31
+ end
32
+
33
+ # Does the current repo have at least a single remote?
34
+ def has_remotes?
35
+ remotes.count > 0
36
+ end
37
+
38
+ # Push to a remote
39
+ #
40
+ # Options:
41
+ # :remote - the name of the remote (default: origin)
42
+ # :ref - the ref to push (default: "refs/heads/master")
43
+ def push(options={})
44
+ raise MissingRemote unless has_remotes?
45
+ options.merge! :remote => DEFAULT_REMOTE, :ref => DEFAULT_LOCAL_REF
46
+ remotes[options[:remote]].push([options[:ref]], {:credentials => credentials})
47
+ end
48
+
49
+ # Fetch a remote
50
+ #
51
+ # Options:
52
+ # remote - the name of the remote (default: origin)
53
+ def fetch(remote=nil)
54
+ raise MissingRemote unless has_remotes?
55
+ repo.fetch(remote || DEFAULT_REMOTE, {:credentials => credentials})
56
+ end
57
+
58
+ # Merge two refs
59
+ #
60
+ # Options:
61
+ # :from - the remote ref (default: "origin/master")
62
+ # :to - the local ref (default: "refs/heads/master")
63
+ def merge(options={})
64
+ options.merge! :from => DEFAULT_REMOTE_BRANCH, :to => DEFAULT_LOCAL_REF
65
+ theirs = repo.rev_parse options[:from]
66
+ ours = repo.rev_parse options[:to]
67
+
68
+ analysis = repo.merge_analysis(theirs)
69
+ return analysis if analysis.include? :up_to_date
70
+
71
+ base = repo.rev_parse(repo.merge_base(ours, theirs))
72
+ index = ours.tree.merge(theirs.tree, base.tree)
73
+
74
+ raise MergeConflict if index.conflicts?
75
+
76
+ Rugged::Commit.create(repo, {
77
+ parents: [ours, theirs],
78
+ tree: index.write_tree(repo),
79
+ message: "Merged `#{options[:from]}` into `#{options[:to].sub("refs/heads/", "")}`",
80
+ update_ref: options[:to]
81
+ })
82
+ end
83
+
84
+ # Fetch a remote and merge
85
+ #
86
+ # Options:
87
+ # :remote - the name of the remote (default: origin)
88
+ # :from - the remote ref (default: "origin/master")
89
+ # :to - the local ref (default: "refs/heads/master")
90
+ def pull(options={})
91
+ fetch(options[:remote])
92
+ merge(options)
93
+ end
94
+
95
+ # Perform both a pull and a push
96
+ #
97
+ # Will fail if any conflicts occur
98
+ def sync
99
+ pull && push
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,3 @@
1
1
  module ChangeAgent
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/script/cibuild CHANGED
@@ -1,12 +1,9 @@
1
1
  #!/bin/sh
2
2
 
3
- git config --get user.name > /dev/null
4
- if [ $? -eq 1 ]; then
5
- git config --global user.name "Your Name"
6
- fi
3
+ set -e
7
4
 
8
- git config --get user.email > /dev/null
9
- if [ $? -eq 1 ]; then
5
+ if [ "$CI" = "true" ]; then
6
+ git config --global user.name "Your Name"
10
7
  git config --global user.email "you@example.com"
11
8
  fi
12
9
 
data/test/helper.rb CHANGED
@@ -6,7 +6,7 @@ require 'shoulda'
6
6
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
7
 
8
8
  def tempdir
9
- File.expand_path "../tmp", File.dirname(__FILE__)
9
+ File.expand_path "./tmp", File.dirname(__FILE__)
10
10
  end
11
11
 
12
12
  def init_tempdir
@@ -5,7 +5,11 @@ class TestChangeAgent < Minitest::Test
5
5
  def setup
6
6
  init_tempdir
7
7
  end
8
-
8
+
9
+ def teardown
10
+ FileUtils.rm_rf tempdir
11
+ end
12
+
9
13
  should "return a ChangeAgent::Client" do
10
14
  assert_equal ChangeAgent::Client, ChangeAgent.init(tempdir).class
11
15
  end
@@ -58,4 +58,11 @@ class TestChangeAgentClient < Minitest::Test
58
58
  should "not err on an unknown value" do
59
59
  refute @client.get "does/not/exist"
60
60
  end
61
+
62
+ should "not double save a document" do
63
+ @client.set "foo", "bar"
64
+ sha = @client.repo.last_commit.oid
65
+ @client.set "foo", "bar"
66
+ assert_equal sha, @client.repo.last_commit.oid
67
+ end
61
68
  end
@@ -33,37 +33,55 @@ class TestChangeAgentDocument < Minitest::Test
33
33
  should "read a file's contents" do
34
34
  @document.contents = "bar"
35
35
  @document.write
36
+ @document.contents = nil # prevent caching
36
37
  assert_equal "bar", @document.contents
37
38
  end
38
39
 
39
40
  should "write a file's contents" do
40
41
  @document.contents = "bar"
41
42
  @document.write
43
+ @document.contents = nil # prevent caching
42
44
  assert_equal "bar", @client.get("foo")
43
45
  end
44
46
 
45
47
  should "commit the document to the repo" do
46
48
  @document.contents = "bar"
47
49
  @document.write
50
+ @document.contents = nil # prevent caching
48
51
  assert_equal "Updating #{@document.key}", @document.repo.last_commit.message
49
52
  end
50
53
 
51
54
  should "delete the document" do
52
55
  @document.contents = "bar"
53
56
  @document.write
57
+ @document.contents = nil # prevent caching
54
58
  assert @client.get "foo"
55
59
  @document.delete
56
60
  refute @client.get "foo"
57
61
  assert_equal "Removing #{@document.key}", @document.repo.last_commit.message
58
62
  end
59
63
 
60
- should "clobber conflicting namespace" do
61
- @document.contents = "bar"
62
- @document.write
63
-
64
+ should "allow two files in the same folder" do
64
65
  doc = ChangeAgent::Document.new("foo/bar", @client)
65
66
  doc.contents = "baz"
66
67
  doc.write
68
+ doc.contents = nil # prevent caching
67
69
  assert_equal "baz", @client.get("foo/bar")
70
+
71
+ doc = ChangeAgent::Document.new("foo/bar2", @client)
72
+ doc.contents = "baz2"
73
+ doc.write
74
+ doc.contents = nil # prevent caching
75
+ assert_equal "baz2", @client.get("foo/bar2")
76
+ end
77
+
78
+ should "know if a file's changed" do
79
+ refute @document.changed?
80
+
81
+ @client.set "foo", "bar"
82
+ refute @document.changed?
83
+
84
+ @document.contents = "baz"
85
+ assert @document.changed?
68
86
  end
69
87
  end
@@ -0,0 +1,62 @@
1
+ require 'helper'
2
+
3
+ class TestChangeAgentSync < Minitest::Test
4
+
5
+ def setup
6
+ init_tempdir
7
+ @client = ChangeAgent::Client.new tempdir
8
+ @demo = ChangeAgent::Client.new tempdir, "http://github.com/benbalter/change_agent_demo"
9
+ end
10
+
11
+ def teardown
12
+ FileUtils.rm_rf tempdir
13
+ end
14
+
15
+ should "return the remotes" do
16
+ assert_equal Rugged::RemoteCollection, @client.remotes.class
17
+ end
18
+
19
+ should "add remotes" do
20
+ assert_equal 0, @client.remotes.count
21
+ @client.add_remote "origin", "https://github.com/benbalter/change_agent_demo"
22
+ assert_equal 1, @client.remotes.count
23
+ end
24
+
25
+ should "know when the repo has remotes" do
26
+ refute @client.has_remotes?
27
+ @client.add_remote "origin", "https://github.com/benbalter/change_agent_demo"
28
+ assert @client.has_remotes?
29
+ end
30
+
31
+ should "fetch" do
32
+ @client.add_remote "origin", "https://github.com/benbalter/change_agent_demo"
33
+ assert_raises Rugged::ReferenceError do
34
+ @client.repo.rev_parse "origin/master"
35
+ end
36
+ @client.fetch
37
+ assert @client.repo.rev_parse "origin/master"
38
+ end
39
+
40
+ should "merge" do
41
+ head = @demo.repo.head.target.oid
42
+ @demo.repo.reset "d877861", :hard
43
+ assert @demo.merge
44
+ assert_equal "Merged `origin/master` into `master`", @demo.repo.last_commit.message
45
+ assert head != @demo.repo.head.target.oid
46
+ end
47
+
48
+ should "pull" do
49
+ head = @demo.repo.head.target.oid
50
+ @demo.repo.reset "d877861", :hard
51
+ assert @demo.pull
52
+ assert_equal "Merged `origin/master` into `master`", @demo.repo.last_commit.message
53
+ assert head != @demo.repo.head.target.oid
54
+ end
55
+
56
+ should "init credentials" do
57
+ ENV["GITHUB_TOKEN"] = "foo"
58
+ assert_equal Rugged::Credentials::UserPassword, @client.credentials.class
59
+ assert_equal "x-oauth-basic", @client.credentials.instance_variable_get("@username")
60
+ assert_equal "foo", @client.credentials.instance_variable_get("@password")
61
+ end
62
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: change_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Balter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-27 00:00:00.000000000 Z
11
+ date: 2014-11-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rugged
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -97,6 +111,7 @@ files:
97
111
  - lib/change_agent.rb
98
112
  - lib/change_agent/client.rb
99
113
  - lib/change_agent/document.rb
114
+ - lib/change_agent/sync.rb
100
115
  - lib/change_agent/version.rb
101
116
  - script/bootstrap
102
117
  - script/cibuild
@@ -106,6 +121,7 @@ files:
106
121
  - test/test_change_agent.rb
107
122
  - test/test_change_agent_client.rb
108
123
  - test/test_change_agent_document.rb
124
+ - test/test_change_agent_sync.rb
109
125
  homepage: https://github.com/benbalter/change-agent
110
126
  licenses:
111
127
  - MIT
@@ -136,3 +152,4 @@ test_files:
136
152
  - test/test_change_agent.rb
137
153
  - test/test_change_agent_client.rb
138
154
  - test/test_change_agent_document.rb
155
+ - test/test_change_agent_sync.rb