1 /*
2 Copyright (C) 2009 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 "git.h"
19 #include "../gource_settings.h"
20
21 // parse git log entries
22
23 //git-log command notes:
24 // - no single quotes on WIN32 as system call treats them differently
25 // - 'user:' prefix allows us to quickly tell if the log is the wrong format
26 // and try a different format (eg cvs-exp)
27
28 int git_version_major = 0;
29 int git_version_minor = 0;
30 int git_version_patch = 0;
31
32 Regex git_version_regex("([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?");
33
readGitVersion()34 void GitCommitLog::readGitVersion() {
35 if(git_version_major != 0) return;
36
37 std::string temp_file;
38 if(!createTempFile(temp_file)) {
39 return;
40 }
41
42 char cmd_buff[2048];
43 int result = snprintf(cmd_buff, sizeof(cmd_buff), "git --version > %s", temp_file.c_str());
44
45 if(result < 0 || result >= sizeof(cmd_buff)) {
46 remove(temp_file.c_str());
47 return;
48 }
49
50 int command_rc = systemCommand(cmd_buff);
51
52 if(command_rc != 0) {
53 remove(temp_file.c_str());
54 return;
55 }
56
57 std::ifstream in(temp_file.c_str());
58
59 if(!in.is_open()) {
60 remove(temp_file.c_str());
61 return;
62 }
63
64 char version_str[1024];
65 in.read(version_str, sizeof(version_str));
66 version_str[sizeof(version_str)-1] = '\0';
67 in.close();
68
69 remove(temp_file.c_str());
70
71 std::vector<std::string> entries;
72 if(!git_version_regex.match(version_str, &entries)) return;
73
74 git_version_major = atoi(entries[0].c_str());
75
76 if(entries.size() > 1) {
77 git_version_minor = atoi(entries[1].c_str());
78 }
79
80 if(entries.size() > 2) {
81 git_version_patch = atoi(entries[2].c_str());
82 }
83 }
84
logCommand()85 std::string GitCommitLog::logCommand() {
86
87 std::string log_command = "git log "
88 "--pretty=format:user:%aN%n%ct "
89 "--reverse --raw --encoding=UTF-8 "
90 "--no-renames";
91
92 readGitVersion();
93
94 // Add --no-show-signature either
95 // if git version couldn't be determined or if version
96 // is at least 2.10
97 if( git_version_major == 0
98 || git_version_major > 2
99 || (git_version_major == 2 && git_version_minor >= 10))
100 {
101 log_command.append(" --no-show-signature");
102 }
103
104 if(!gGourceSettings.start_date.empty()) {
105 log_command += " --since ";
106 log_command += gGourceSettings.start_date;
107 }
108
109 if(!gGourceSettings.stop_date.empty()) {
110 log_command += " --until ";
111 log_command += gGourceSettings.stop_date;
112 }
113
114 if(!gGourceSettings.git_branch.empty()) {
115 log_command += " ";
116 log_command += gGourceSettings.git_branch;
117 }
118
119 return log_command;
120 }
121
GitCommitLog(const std::string & logfile)122 GitCommitLog::GitCommitLog(const std::string& logfile) : RCommitLog(logfile, 'u') {
123
124 log_command = logCommand();
125
126 //can generate log from directory
127 if(!logf && is_dir) {
128 logf = generateLog(logfile);
129
130 if(logf) {
131 success = true;
132 seekable = true;
133 }
134 }
135 }
136
generateLog(const std::string & dir)137 BaseLog* GitCommitLog::generateLog(const std::string& dir) {
138 //get working directory
139 char cwd_buff[1024];
140
141 if(getcwd(cwd_buff, 1024) != cwd_buff) {
142 return 0;
143 }
144
145 //does directory have a .git ?
146 std::string gitdir = dir + std::string("/.git");
147 struct stat dirinfo;
148 int stat_rc = stat(gitdir.c_str(), &dirinfo);
149 if(stat_rc!=0 || !(dirinfo.st_mode & S_IFDIR || dirinfo.st_mode & S_IFREG)) {
150 return 0;
151 }
152
153 // do we have this client installed
154 requireExecutable("git");
155
156 std::string command = getLogCommand();
157
158 //create temp file
159 createTempLog();
160
161 if(temp_file.size()==0) return 0;
162
163 if(chdir(dir.c_str()) != 0) {
164 return 0;
165 }
166
167 char cmd_buff[2048];
168 int written = snprintf(cmd_buff, 2048, "%s > %s", command.c_str(), temp_file.c_str());
169
170 if(written < 0 || written >= 2048) {
171 return 0;
172 }
173
174 int command_rc = systemCommand(cmd_buff);
175
176 if(command_rc != 0) {
177 chdir(cwd_buff);
178 return 0;
179 }
180
181 // check for new-enough Git version
182 // if %aN does not appear to be supported try %an
183 std::ifstream in(temp_file.c_str());
184 char firstBytes[9];
185 in.read(firstBytes, 8);
186 in.close();
187 firstBytes[8] = '\0';
188 if(!strcmp(firstBytes, "user:%aN")) {
189 char *pos = strstr(cmd_buff, "%aN");
190 pos[2] = 'n';
191 command_rc = systemCommand(cmd_buff);
192 }
193
194 //change back to original directoy
195 chdir(cwd_buff);
196
197 if(command_rc != 0) {
198 return 0;
199 }
200
201 BaseLog* seeklog = new SeekLog(temp_file);
202
203 return seeklog;
204 }
205
206 // parse modified git format log entries
207
parseCommit(RCommit & commit)208 bool GitCommitLog::parseCommit(RCommit& commit) {
209
210 std::string line;
211
212 commit.username = "";
213
214 while(logf->getNextLine(line) && line.size()) {
215
216 if(line.find("user:") == 0) {
217
218 //username follows user prefix
219 commit.username = line.substr(5);
220
221 if(!logf->getNextLine(line)) return false;
222
223 commit.timestamp = atol(line.c_str());
224
225 //this isnt a commit we are parsing, abort
226 if(commit.timestamp == 0) return false;
227
228 continue;
229 }
230
231 //should see username before files
232 if(commit.username.empty()) return false;
233
234 size_t tab = line.find('\t');
235
236 //incorrect log format
237 if(tab == std::string::npos || tab == 0 || tab == line.size()-1) continue;
238
239 std::string status = line.substr(tab - 1, 1);
240 std::string file = line.substr(tab + 1);
241
242 if(file.empty()) continue;
243
244 //check for and remove double quotes
245 if(file.find('"') == 0 && file.rfind('"') == file.size()-1) {
246 if(file.size()<=2) continue;
247
248 file = file.substr(1,file.size()-2);
249 }
250
251 commit.addFile(file, status);
252 }
253
254 //check we at least got a username
255 if(commit.username.empty()) return false;
256
257 return true;
258 }
259