parse logs with commits that have no line changes
[phorkie.git] / src / phorkie / Repository.php
1 <?php
2 namespace phorkie;
3
4
5 class Repository
6 {
7     /**
8      * Repository ID (number in repositories directory)
9      *
10      * @var integer
11      */
12     public $id;
13
14     /**
15      * Full path to the .git repository
16      *
17      * @var string
18      */
19     public $gitDir;
20
21     /**
22      * Full path to the work tree directory
23      *
24      * @var string
25      */
26     public $workDir;
27
28     /**
29      * Revision of the repository that shall be shown
30      *
31      * @var string
32      */
33     public $hash;
34
35     /**
36      * Commit message of the last (or current) revision
37      *
38      * @var string
39      */
40     public $message;
41
42
43     /**
44      * Load Repository data from GET-Request
45      *
46      * @return void
47      *
48      * @throws Exception When something is wrong
49      */
50     public function loadFromRequest()
51     {
52         if (!isset($_GET['id'])) {
53             throw new Exception_Input('Paste ID missing');
54         }
55         if (!is_numeric($_GET['id'])) {
56             throw new Exception_Input('Paste ID not numeric');
57         }
58         if (isset($_GET['rev'])) {
59             $this->hash = $_GET['rev'];
60         }
61
62         $this->id = (int)$_GET['id'];
63         $this->loadDirs();
64         $this->loadHash();
65         $this->loadMessage();
66     }
67
68     protected function loadDirs()
69     {
70         $gitDir = $GLOBALS['phorkie']['cfg']['gitdir'] . '/' . $this->id . '.git';
71         if (!is_dir($gitDir)) {
72             throw new Exception_NotFound(
73                 sprintf('Paste %d .git dir not found', $this->id)
74             );
75         }
76         $this->gitDir = $gitDir;
77
78         $workDir = $GLOBALS['phorkie']['cfg']['workdir'] . '/' . $this->id;
79         if (!is_dir($workDir)) {
80             throw new Exception_NotFound(
81                 sprintf('Paste %d work dir not found', $this->id)
82             );
83         }
84         $this->workDir = $workDir;
85     }
86
87     public function loadHash()
88     {
89         return;
90         if ($this->hash !== null) {
91             return;
92         }
93
94         $output = $this->getVc()->getCommand('log')
95             ->setOption('pretty', 'format:%H')
96             ->setOption('max-count', 1)
97             ->execute();
98         $output = trim($output);
99         if (strlen($output) !== 40) {
100             throw new Exception(
101                 'Loading commit hash failed: ' . $output
102             );
103         }
104         $this->hash = $output;
105     }
106
107         /**
108      * Populates $this->message
109      *
110      * @return void
111      */
112     public function loadMessage()
113     {
114         $rev = (isset($this->hash)) ? $this->hash : 'HEAD';
115         $output = $this->getVc()->getCommand('log')
116             ->setOption('oneline')
117             ->addArgument('-1')
118             ->addArgument($rev)
119             ->execute();
120         $output = trim($output);
121         if (strpos($output, ' ') > 0) {
122             $output = substr($output, strpos($output, ' '), strlen($output));
123             $this->message = trim($output);
124         } else {
125             $this->message = "This commit message intentionally left blank.";
126         }
127     }
128
129     public function loadById($id)
130     {
131         if (!is_numeric($id)) {
132             throw new Exception_Input('Paste ID not numeric');
133         }
134         $this->id = (int)$id;
135         $this->loadDirs();
136         $this->loadHash();
137     }
138
139     public function getVc()
140     {
141         return new \VersionControl_Git($this->gitDir);
142     }
143
144     /**
145      * Loads the list of files in this repository
146      *
147      * @return File[] Array of files
148      */
149     public function getFiles()
150     {
151         $files = $this->getFilePaths();
152         $arFiles = array();
153         foreach ($files as $name) {
154             $arFiles[] = new File($name, $this);
155         }
156         return $arFiles;
157     }
158
159     protected function getFilePaths()
160     {
161         if ($this->hash === null) {
162             $hash = 'HEAD';
163         } else {
164             $hash = $this->hash;
165         }
166         $output = $this->getVc()->getCommand('ls-tree')
167             ->setOption('r')
168             ->setOption('name-only')
169             ->addArgument($hash)
170             ->execute();
171         return explode("\n", trim($output));
172     }
173
174     public function getFileByName($name, $bHasToExist = true)
175     {
176         $name = Tools::sanitizeFilename($name);
177         if ($name == '') {
178             throw new Exception_Input('Empty file name given');
179         }
180
181         if ($bHasToExist) {
182             $files = $this->getFilePaths();
183             if (array_search($name, $files) === false) {
184                 throw new Exception_Input('File does not exist');
185             }
186         }
187         return new File($name, $this);
188     }
189
190     public function hasFile($name)
191     {
192         try {
193             $this->getFileByName($name);
194         } catch (Exception $e) {
195             return false;
196         }
197         return true;
198     }
199
200     /**
201      * Permanently deletes the paste repository without any way to get
202      * it back.
203      *
204      * @return boolean True if all went well, false if not
205      */
206     public function delete()
207     {
208         $db = new Database();
209         $db->getIndexer()->deleteRepo($this);
210
211         return Tools::recursiveDelete($this->workDir)
212             && Tools::recursiveDelete($this->gitDir);
213     }
214
215     public function getTitle()
216     {
217         $desc = $this->getDescription();
218         if (trim($desc) != '') {
219             return $desc;
220         }
221
222         return 'paste #' . $this->id;
223     }
224
225     public function getDescription()
226     {
227         if (!is_readable($this->gitDir . '/description')) {
228             return null;
229         }
230         return file_get_contents($this->gitDir . '/description');
231     }
232
233     public function setDescription($description)
234     {
235         file_put_contents($this->gitDir . '/description', $description);
236     }
237
238     /**
239      * Get a link to the repository
240      *
241      * @param string $type Link type. Supported are:
242      *                     - "edit"
243      *                     - "delete"
244      *                     - "delete-confirm"
245      *                     - "display"
246      *                     - "fork"
247      *                     - "revision"
248      * @param string $option
249      *
250      * @return string
251      */
252     public function getLink($type, $option = null)
253     {
254         if ($type == 'edit') {
255             return '/' . $this->id . '/edit';
256         } else if ($type == 'display') {
257             return '/' . $this->id;
258         } else if ($type == 'fork') {
259             return '/' . $this->id . '/fork';
260         } else if ($type == 'doap') {
261             return '/' . $this->id . '/doap';
262         } else if ($type == 'delete') {
263             return '/' . $this->id . '/delete';
264         } else if ($type == 'delete-confirm') {
265             return '/' . $this->id . '/delete/confirm';
266         } else if ($type == 'revision') {
267             return '/' . $this->id . '/rev/' . $option;
268         }
269         throw new Exception('Unknown link type');
270     }
271
272     public function getCloneURL($public = true)
273     {
274         $var = $public ? 'public' : 'private';
275         if (isset($GLOBALS['phorkie']['cfg']['git'][$var])) {
276             return $GLOBALS['phorkie']['cfg']['git'][$var] . $this->id . '.git';
277         }
278         return null;
279     }
280
281     /**
282      * Returns the history of the repository.
283      * We don't use VersionControl_Git's rev list fetcher since it does not
284      * give us separate email addresses and names, and it does not give us
285      * the amount of changed (added/deleted) lines.
286      *
287      * @return array Array of history objects
288      */
289     public function getHistory()
290     {
291         $output = $this->getVc()->getCommand('log')
292             ->setOption('pretty', 'format:commit %H%n%at%n%an%n%ae')
293             ->setOption('max-count', 10)
294             ->setOption('shortstat')
295             ->execute();
296
297         $arCommits = array();
298         $arOutput = explode("\n", $output);
299         $lines = count($arOutput);
300         $current = 0;
301         while ($current < $lines) {
302             $commit = new Repository_Commit();
303             list($name,$commit->hash) = explode(' ', $arOutput[$current]);
304             if ($name !== 'commit') {
305                 throw new Exception(
306                     'Git log output format not as expected: ' . $arOutput[$current]
307                 );
308             }
309             $commit->committerTime  = $arOutput[$current + 1];
310             $commit->committerName  = $arOutput[$current + 2];
311             $commit->committerEmail = $arOutput[$current + 3];
312
313             if (substr($arOutput[$current + 4], 0, 1) != ' ') {
314                 //commit without changed lines
315                 $arCommits[] = $commit;
316                 $current += 4;
317                 continue;
318             }
319
320             $arLineParts = explode(' ', trim($arOutput[$current + 4]));
321             $commit->filesChanged = $arLineParts[0];
322             $commit->linesAdded   = $arLineParts[3];
323             if (isset($arLineParts[5])) {
324                 $commit->linesDeleted = $arLineParts[5];
325             }
326
327             $current += 6;
328
329             $arCommits[] = $commit;
330         }
331
332         return $arCommits;
333     }
334 }
335
336 ?>