001: /*****************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2007, ThoughtWorks, Inc.
004: * 200 E. Randolph, 25th Floor
005: * Chicago, IL 60601 USA
006: * All rights reserved.
007: *
008: * Redistribution and use in source and binary forms, with or without
009: * modification, are permitted provided that the following conditions
010: * are met:
011: *
012: * + Redistributions of source code must retain the above copyright
013: * notice, this list of conditions and the following disclaimer.
014: *
015: * + Redistributions in binary form must reproduce the above
016: * copyright notice, this list of conditions and the following
017: * disclaimer in the documentation and/or other materials provided
018: * with the distribution.
019: *
020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
021: * names of its contributors may be used to endorse or promote
022: * products derived from this software without specific prior
023: * written permission.
024: *
025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
036: *****************************************************************************/package net.sourceforge.cruisecontrol.sourcecontrols;
037:
038: import net.sourceforge.cruisecontrol.CruiseControlException;
039: import net.sourceforge.cruisecontrol.Modification;
040: import net.sourceforge.cruisecontrol.SourceControl;
041: import net.sourceforge.cruisecontrol.util.Commandline;
042: import net.sourceforge.cruisecontrol.util.IO;
043: import net.sourceforge.cruisecontrol.util.StreamLogger;
044: import net.sourceforge.cruisecontrol.util.Util;
045: import net.sourceforge.cruisecontrol.util.ValidationHelper;
046: import org.apache.log4j.Logger;
047:
048: import java.io.BufferedReader;
049: import java.io.File;
050: import java.io.InputStreamReader;
051: import java.io.IOException;
052: import java.io.Reader;
053: import java.util.ArrayList;
054: import java.util.Date;
055: import java.util.List;
056: import java.util.Map;
057: import java.util.regex.Pattern;
058: import java.util.regex.Matcher;
059:
060: /**
061: * This class implements the SourceControl methods for a git repository. The
062: * call to git is assumed to work without any setup. This implies that
063: * authentication data must be available.
064: *
065: * @see <a href="http://git.or.cz/">git.or.cz</a>
066: * @author <a href="rschiele@gmail.com">Robert Schiele</a>
067: */
068: public class Git implements SourceControl {
069: private static final Logger LOG = Logger.getLogger(Git.class);
070: private static final Pattern COMMITPATTERN = Pattern
071: .compile("commit ([0-9a-f]{40})");
072: private static final Pattern AUTHORPATTERN = Pattern
073: .compile("author (.*) <(.*)> ([0-9]*) [+-][0-9]{4}");
074: private static final Pattern DIFFPATTERN = Pattern
075: .compile("diff --git (a/.* b/.*)");
076: private static final Pattern NEWFILEPATTERN = Pattern
077: .compile("new file mode [0-7]{6}");
078: private static final Pattern DELETEDFILEPATTERN = Pattern
079: .compile("deleted file mode [0-7]{6}");
080: private static final String NEWLINE = System
081: .getProperty("line.separator");
082:
083: private final SourceControlProperties props = new SourceControlProperties();
084: private String lwc;
085:
086: public Map getProperties() {
087: return props.getPropertiesAndReset();
088: }
089:
090: public void setProperty(String p) {
091: props.assignPropertyName(p);
092: }
093:
094: public void setPropertyOnDelete(String p) {
095: props.assignPropertyOnDeleteName(p);
096: }
097:
098: /**
099: * Sets the local working copy to use when making calls to git.
100: *
101: * @param d String indicating the relative or absolute path to the local
102: * working copy of the git repository of which to find the log history.
103: */
104: public void setLocalWorkingCopy(String d) {
105: lwc = d;
106: }
107:
108: /**
109: * This method validates that the local working copy location has been
110: * specified.
111: *
112: * @throws CruiseControlException Thrown when the local working copy
113: * location is null
114: */
115: public void validate() throws CruiseControlException {
116: ValidationHelper.assertTrue(lwc != null,
117: "'localWorkingCopy' is a required "
118: + "attribute on the Git task");
119:
120: final File wd = new File(lwc);
121: ValidationHelper.assertTrue(wd.exists() && wd.isDirectory(),
122: "'localWorkingCopy' must be an existing "
123: + "directory. Was " + wd.getAbsolutePath());
124: }
125:
126: /**
127: * Returns a list of modifications detailing all the changes between the
128: * last build and the latest revision in the repository.
129: * @return the list of modifications, or an empty list if we failed to
130: * retrieve the changes.
131: */
132: public List getModifications(Date from, Date to) {
133: final List mods = new ArrayList();
134: final Commandline cmd = new Commandline();
135: cmd.setExecutable("git");
136: try {
137: cmd.setWorkingDirectory(lwc);
138: } catch (CruiseControlException e) {
139: LOG.error("Error building history command", e);
140: return mods;
141: }
142: cmd.createArgument("log");
143: cmd.createArgument("-p");
144: cmd.createArgument("--pretty=raw");
145: cmd.createArgument(gitRevision(from) + ".." + gitRevision(to));
146: LOG.debug("Executing command: " + cmd);
147: try {
148: final Process p = cmd.execute();
149: final Thread stderr = new Thread(StreamLogger
150: .getWarnPumper(LOG, p.getErrorStream()));
151: stderr.start();
152: parseLog(
153: new InputStreamReader(p.getInputStream(), "UTF-8"),
154: mods, props);
155: p.waitFor();
156: stderr.join();
157: IO.close(p);
158: } catch (Exception e) {
159: LOG.error("Error executing git log command " + cmd, e);
160: }
161: return mods;
162: }
163:
164: static void parseLog(Reader grd, List mods,
165: SourceControlProperties props) throws IOException {
166: BufferedReader rd = new BufferedReader(grd);
167: boolean diffmode = false;
168: Modification mod = null;
169: while (true) {
170: String l = rd.readLine();
171: if (l == null) {
172: break;
173: }
174: if (l.equals("")) {
175: /* If in diff mode this ends the diff mode. Otherwise it
176: starts the comment block. */
177: if (diffmode) {
178: diffmode = false;
179: } else {
180: mod.comment = "";
181: while (true) {
182: l = rd.readLine();
183: if (l == null || l.equals("")) {
184: break;
185: }
186: mod.comment += l.substring(4) + NEWLINE;
187: }
188: }
189: continue;
190: }
191: Matcher matcher = COMMITPATTERN.matcher(l);
192: if (matcher.matches()) {
193: /* If this is the latest modification store commit id as
194: property. */
195: if (mod == null) {
196: props.put("gitcommitid", matcher.group(1));
197: }
198: mod = new Modification("git");
199: mods.add(mod);
200: props.modificationFound();
201: continue;
202: }
203: matcher = AUTHORPATTERN.matcher(l);
204: if (matcher.matches()) {
205: mod.userName = matcher.group(1);
206: mod.emailAddress = matcher.group(2);
207: final long dt = new Long(matcher.group(3)).longValue();
208: /* Set revision to commit date. */
209: mod.revision = "" + dt;
210: mod.modifiedTime = new Date(dt * 1000);
211: continue;
212: }
213: matcher = DIFFPATTERN.matcher(l);
214: if (matcher.matches()) {
215: final String m1 = matcher.group(1);
216: final Modification.ModifiedFile modfile = mod
217: .createModifiedFile(m1
218: .substring(m1.length() / 2 + 3), null);
219: l = rd.readLine();
220: if (DELETEDFILEPATTERN.matcher(l).matches()) {
221: modfile.action = "deleted";
222: props.deletionFound();
223: } else {
224: modfile.action = NEWFILEPATTERN.matcher(l)
225: .matches() ? "added" : "modified";
226: }
227: modfile.revision = mod.revision;
228: /* Remember we are in diffmode. Parser needs this information
229: to handle empty lines correctly. */
230: diffmode = true;
231: continue;
232: }
233: }
234: }
235:
236: static String gitRevision(Date dt) {
237: final String dts = "@{ " + (dt.getTime() / 1000) + "}";
238: /* The SVN plugin claims we have to quote this for Windows. */
239: return Util.isWindows() ? ("\"" + dts + "\"") : dts;
240: }
241: }
|