1 /*
2     Copyright (C) 2010 Andrew Caudwell (acaudwell@gmail.com)
3 
4     This program is free software; you can redistribute it and/or
5     modify it under the terms of the GNU General Public License
6     as published by the Free Software Foundation; either version
7     3 of the License, or (at your option) any later version.
8 
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13 
14     You should have received a copy of the GNU General Public License
15     along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17 
18 #include "svn.h"
19 #include "../gource_settings.h"
20 
21 #include <boost/format.hpp>
22 
23 #ifdef HAVE_LIBTINYXML
24 #include <tinyxml.h>
25 #else
26 #include "../tinyxml/tinyxml.h"
27 #endif
28 
29 Regex svn_xml_tag("^<\\??xml");
30 Regex svn_logentry_start("^<logentry");
31 Regex svn_logentry_end("^</logentry>");
32 Regex svn_logentry_timestamp("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})");
33 
logCommand()34 std::string SVNCommitLog::logCommand() {
35 
36     std::string start = (!gGourceSettings.start_date.empty())
37         ? str(boost::format("{%s}") % gGourceSettings.start_date) : "1";
38 
39     std::string stop  = (!gGourceSettings.stop_date.empty())
40         ? str(boost::format("{%s}") % gGourceSettings.stop_date) : "HEAD";
41 
42     std::string range = str(boost::format("%s:%s") % start % stop);
43 
44     std::string log_command = str(boost::format("svn log -r %s --xml --verbose --quiet") % range);
45 
46     return log_command;
47 }
48 
SVNCommitLog(const std::string & logfile)49 SVNCommitLog::SVNCommitLog(const std::string& logfile) : RCommitLog(logfile, '<') {
50 
51     log_command = logCommand();
52 
53     //can generate log from directory
54     if(!logf && is_dir) {
55         logf = generateLog(logfile);
56 
57         if(logf) {
58             success  = true;
59             seekable = true;
60         }
61     }
62 
63     logentry.reserve(1024);
64 }
65 
66 
generateLog(const std::string & dir)67 BaseLog* SVNCommitLog::generateLog(const std::string& dir) {
68     //get working directory
69     char cwd_buff[1024];
70 
71     if(getcwd(cwd_buff, 1024) != cwd_buff) {
72         return 0;
73     }
74 
75     //does directory have a .svn ?
76     std::string gitdir = dir + std::string("/.svn");
77     struct stat dirinfo;
78     int stat_rc = stat(gitdir.c_str(), &dirinfo);
79     if(stat_rc!=0 || !(dirinfo.st_mode & S_IFDIR)) {
80         return 0;
81     }
82 
83     // do we have this client installed
84     requireExecutable("svn");
85 
86     std::string command = getLogCommand();
87 
88     //create temp file
89     createTempLog();
90 
91     if(temp_file.size()==0) return 0;
92 
93     if(chdir(dir.c_str()) != 0) {
94         return 0;
95     }
96 
97     char cmd_buff[2048];
98     snprintf(cmd_buff, 2048, "%s > %s", command.c_str(), temp_file.c_str());
99 
100     int command_rc = systemCommand(cmd_buff);
101 
102     chdir(cwd_buff);
103 
104     if(command_rc != 0) {
105         return 0;
106     }
107 
108     BaseLog* seeklog = new SeekLog(temp_file);
109 
110     return seeklog;
111 }
112 
113 #ifndef HAVE_TIMEGM
114 
115 std::string system_tz;
116 bool system_tz_init = false;
117 
__timegm_hack(struct tm * tm)118 time_t __timegm_hack(struct tm* tm) {
119 
120     if(!system_tz_init) {
121         char* current_tz_env = getenv("TZ");
122 
123         if(current_tz_env != 0) {
124             system_tz  = std::string("TZ=") + current_tz_env;
125         }
126 
127         system_tz_init = true;
128     }
129 
130     putenv((char*)"TZ=UTC");
131     tzset();
132 
133     time_t timestamp = mktime(tm);
134 
135     if(!system_tz.empty()) {
136         putenv((char*)system_tz.c_str());
137     } else {
138 #ifdef HAVE_UNSETENV
139         unsetenv("TZ");
140 #else
141         putenv((char*)"TZ=");
142 #endif
143     }
144     tzset();
145 
146     return timestamp;
147 }
148 #endif
149 
parseCommit(RCommit & commit)150 bool SVNCommitLog::parseCommit(RCommit& commit) {
151 
152     //fprintf(stderr,"parsing svn log\n");
153 
154     std::string line;
155 
156     if(!getNextLine(line)) return false;
157 
158     //start of log entry
159     if(!svn_logentry_start.match(line)) {
160 
161         //is this the start of the document
162         if(!svn_xml_tag.match(line)) return false;
163 
164         //fprintf(stderr,"found xml tag\n");
165 
166         //if so find the first logentry tag
167 
168         bool found_logentry = false;
169 
170         while(getNextLine(line)) {
171             if(svn_logentry_start.match(line)) {
172                 found_logentry = true;
173                 break;
174             }
175         }
176 
177         if(!found_logentry) return false;
178     }
179 
180     //fprintf(stderr,"found logentry\n");
181 
182     logentry.clear();
183 
184     logentry.append(line);
185     logentry.append("\n");
186 
187     //fprintf(stderr,"found opening tag\n");
188 
189     bool endfound = false;
190 
191     while(getNextLine(line)) {
192         logentry.append(line);
193         logentry.append("\n");
194         if(svn_logentry_end.match(line)) {
195             //fprintf(stderr,"found closing tag\n");
196             endfound=true;
197             break;
198         }
199     }
200 
201     //incomplete commit
202     if(!endfound) return false;
203 
204     //fprintf(stderr,"read logentry\n");
205 
206     TiXmlDocument doc;
207 
208     if(!doc.Parse(logentry.c_str())) return false;
209 
210     //fprintf(stderr,"try to parse logentry: %s\n", logentry.c_str());
211 
212     TiXmlElement* leE = doc.FirstChildElement( "logentry" );
213 
214     std::vector<std::string> entries;
215 
216     if(!leE) return false;
217 
218     //parse date
219     TiXmlElement* dateE = leE->FirstChildElement( "date" );
220 
221     if(!dateE) return false;
222 
223     std::string timestamp_str(dateE->GetText());
224 
225     if(!svn_logentry_timestamp.match(timestamp_str, &entries))
226         return false;
227 
228     struct tm time_str;
229 
230     time_str.tm_year  = atoi(entries[0].c_str()) - 1900;
231     time_str.tm_mon   = atoi(entries[1].c_str()) - 1;
232     time_str.tm_mday  = atoi(entries[2].c_str());
233     time_str.tm_hour  = atoi(entries[3].c_str());
234     time_str.tm_min   = atoi(entries[4].c_str());
235     time_str.tm_sec   = atoi(entries[5].c_str());
236     time_str.tm_isdst = -1;
237 
238 #ifdef HAVE_TIMEGM
239     commit.timestamp = timegm(&time_str);
240 #else
241     commit.timestamp = __timegm_hack(&time_str);
242 #endif
243 
244     //parse author
245     TiXmlElement* authorE = leE->FirstChildElement("author");
246 
247     if(authorE != 0) {
248         // GetText() may return NULL, causing author instantiation to crash.
249         std::string author;
250         if(authorE->GetText()) author = authorE->GetText();
251         if(author.empty()) author = "Unknown";
252 
253         commit.username = author;
254     }
255 
256     TiXmlElement* pathsE = leE->FirstChildElement( "paths" );
257 
258     //log entries sometimes dont have any paths
259     if(!pathsE) return true;
260 
261     //parse changes
262 
263     for(TiXmlElement* pathE = pathsE->FirstChildElement("path"); pathE != 0; pathE = pathE->NextSiblingElement()) {
264         //parse path
265 
266         const char* kind   = pathE->Attribute("kind");
267         const char* action = pathE->Attribute("action");
268 
269         //check for action
270         if(action == 0) continue;
271 
272         bool is_dir = false;
273 
274         //if has the 'kind' attribute (old versions of svn dont have this), check if it is a dir
275         if(kind != 0 && strcmp(kind,"dir") == 0) {
276 
277             //accept only deletes for directories
278             if(strcmp(action, "D") != 0) continue;
279 
280             is_dir = true;
281         }
282 
283         std::string file(pathE->GetText());
284         std::string status(action);
285 
286         if(file.empty()) continue;
287         if(status.empty()) continue;
288 
289         //append trailing slash if is directory
290         if(is_dir && file[file.size()-1] != '/') {
291             file = file + std::string("/");
292         }
293 
294         commit.addFile(file, status);
295     }
296 
297     //fprintf(stderr,"parsed logentry\n");
298 
299     //read files
300 
301     return true;
302 }
303