Problem
A developer fails to commit changes to a source repository while discovering they have been working on a locked or stale resource. Productivity and moral decrease as said developer merges his or her work with that of other teammates.
$svn -m "stale resource" ci stale_resource.txt
Sending stale_resource.txt
svn: Commit failed (details follow):
svn: Your file or directory 'stale_resource.txt' is
probably out-of-date
Cause
Distributed, mismanaged and/or large teams often have communication challenges. Developers who don’t "feel" Continuous Integration often receive headaches after long periods of time without a commit or update.
Solution
Conflict is an open source project consisting of two parts: Conflict clients and a Conflict server. A Conflict client is a transparent background process running in the development environment. It uses a versioning client to gather a diff of the local checkout and posts the diff to the Conflict server. The Conflict server keeps track of this information, notifying each developer of conflicts via the response.
Demonstration
Two developers begin editing the same resource. The first developer adds the following line to a javadoc for a unit test.
$svn diff
Index: org/apache/shale/test/base/AbstractJsfTestCase.java
===============================================================
--- org/apache/shale/test/base/AbstractJsfTestCase.java (revision 553360)
+++ org/apache/shale/test/base/AbstractJsfTestCase.java (working copy)
@@ -92,6 +92,7 @@
/**
* Set up instance variables required by this test case.
+ * I'm in jail!
*/
protected void setUp() throws Exception {
Another developer adds a line of code to the javadocs as well.
$svn diff
Index: org/apache/shale/test/base/AbstractJsfTestCase.java
===============================================================
--- org/apache/shale/test/base/AbstractJsfTestCase.java (revision 553360)
+++ org/apache/shale/test/base/AbstractJsfTestCase.java (working copy)
@@ -92,6 +92,7 @@
/**
* Set up instance variables required by this test case.
+ * I am the greatest!
*/>
protected void setUp() throws Exception {
The last developer to commit will be stuck with the merge. The longer these two wait to update or commit, the higher the costs – especially when the conflict spreads to business logic and/or integration points. The relationship between cost and time tends to exponential, not linear.
To demonstrate how these two developers can “fail fast” I’ve thrown together two clients, one in Java and the other in Ruby. They are located at the bottom of this blog post.
Using the Ruby poller:
irb(main):001:0> load 'conflict.rb'
irb(main):002:0> include Conflict
irb(main):003:0> client = Client::new({:id=>"paris hilton",:port=>81})
=> #<Conflict::Client:0x2df19b4 @cfg={:url_resource=>"/diff", :poll=>3, :command
=>"svn diff", :port=>81, :id=>"paris hilton", :ip=>"127.0.0.1"}>
irb(main):004:0> client.poll
--- []
--- []
--- []
The body of a Conflict server response is YAML. All three responses contain empty YAML notation, indicating no conflicts.
Using the Java poller:
$java com.thoughtworks.conflict.Client "muhammad ali" 127.0.0.1 81 "/diff" 3000
Now both clients poll the file system and send a diff to the Conflict server. The Conflict server now begins to report the conflict in the response for each client.
The response for the first developer:
---
- !Conflict
local: !Event
action: changed
client: paris hilton
resource: org/apache/shale/test/base/AbstractJsfTestCase.java
remote: !Event
action: changed
client: muhammad ali
resource: org/apache/shale/test/base/AbstractJsfTestCase.java
The response for the second developer is the inverse:
---
- !Conflict
local: !Event
action: changed
client: muhammad ali
resource: org/apache/shale/test/base/AbstractJsfTestCase.java
remote: !Event
action: changed
client: paris hilton
resource: org/apache/shale/test/base/AbstractJsfTestCase.java
Future
The idea of Continuous Integration has been around for awhile, but the industry could be taking it farther. Continuous Integration is more than just installing Cruise Control. Developers often overlook the part about frequent commits, something I call micro-committing.
The Conflict server code is in pretty decent shape for now, but it is useless without good clients. While the poller clients of this blog post do a good job of demonstrating the idea, the real value is in IDE integration. Next on my plate is an Eclipse RCP plugin. After that perhaps a web based reporting feature for scrum masters, iteration managers, tech leads or anyone else interested in a summary of the trenches.
class Client
def initialize cfg={}
@cfg = {}
@cfg[:command] = cfg[:command] ||= "svn diff"
@cfg[:id] = cfg[:id] ||= "Conflict Ruby client"
@cfg[:ip] = cfg[:ip] ||= "127.0.0.1"
@cfg[:port] = cfg[:port] ||= PORT
@cfg[:url_resource] = cfg[:url_resource] ||= URL_RESOURCE
@cfg[:poll] = cfg[:poll] ||= 3
end
def send diff
req = Net::HTTP::Post.new(@cfg[:url_resource])
http = Net::HTTP.new(@cfg[:ip],@cfg[:port])
res = http.request(req, "client=" + @cfg[:id] + "&diff=" + diff)
res.body
end
def poll
while true
puts send(`#{@cfg[:command]}`)
sleep @cfg[:poll]
end
end
end
public class Client {
private String[] command = new String[] { "svn", "diff" };
private String client;
private String ip;
private Integer port;
private String urlResource;
private Integer poll;
public Client(String client, String ip,
Integer port, String urlResource, Integer poll) {
this.client = client == null ? "Conflict Java client" : client;
this.ip = ip == null ? "127.0.0.1" : ip;
this.port = port == null ? 80 : port;
this.urlResource = urlResource == null ? "/diff" : urlResource;
this.poll = poll == null ? 3000 : poll;
}
public static void main(final String[] args) throws Exception {
Client client = new Client(args[0], args[1], Integer.parseInt(args[2]),
args[3], Integer.parseInt(args[4]));
client.poll();
}
public void poll() throws Exception {
while (true) {
String diff = pollFileSystem();
pollConflictServer(diff);
Thread.sleep(poll);
}
}
private String pollConflictServer(String diff) throws IOException {
HttpConnection connection = new HttpConnection(ip, port);
connection.open();
PostMethod post = new PostMethod(urlResource);
NameValuePair[] data = {
new NameValuePair("client", client),
new NameValuePair("diff", diff) };
post.setRequestBody(data);
post.execute(new HttpState(), connection);
String response = post.getResponseBodyAsString();
connection.close();
return response;
}
private String pollFileSystem() throws IOException {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String output = "", line = null;
while ((line = reader.readLine()) != null) {
output += line + '\n';
}
return output;
}
}

0 comments:
Post a Comment