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