simple cache for rendered files
[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     public function loadById($id)
69     {
70         if (!is_numeric($id)) {
71             throw new Exception_Input('Paste ID not numeric');
72         }
73         $this->id = (int)$id;
74         $this->loadDirs();
75         $this->loadHash();
76     }
77
78     protected function loadDirs()
79     {
80         $gitDir = $GLOBALS['phorkie']['cfg']['gitdir'] . '/' . $this->id . '.git';
81         if (!is_dir($gitDir)) {
82             throw new Exception_NotFound(
83                 sprintf('Paste %d .git dir not found', $this->id)
84             );
85         }
86         $this->gitDir = $gitDir;
87
88         $workDir = $GLOBALS['phorkie']['cfg']['workdir'] . '/' . $this->id;
89         if (!is_dir($workDir)) {
90             throw new Exception_NotFound(
91                 sprintf('Paste %d work dir not found', $this->id)
92             );
93         }
94         $this->workDir = $workDir;
95     }
96
97     public function loadHash()
98     {
99         if ($this->hash !== null) {
100             return;
101         }
102
103         $output = $this->getVc()->getCommand('log')
104             ->setOption('pretty', 'format:%H')
105             ->setOption('max-count', 1)
106             ->execute();
107         $output = trim($output);
108         if (strlen($output) !== 40) {
109             throw new Exception(
110                 'Loading commit hash failed: ' . $output
111             );
112         }
113         $this->hash = $output;
114     }
115
116     /**
117      * Populates $this->message
118      *
119      * @return void
120      */
121     public function loadMessage()
122     {
123         $rev = (isset($this->hash)) ? $this->hash : 'HEAD';
124         $output = $this->getVc()->getCommand('log')
125             ->setOption('oneline')
126             ->addArgument('-1')
127             ->addArgument($rev)
128             ->execute();
129         $output = trim($output);
130         if (strpos($output, ' ') > 0) {
131             $output = substr($output, strpos($output, ' '), strlen($output));
132             $this->message = trim($output);
133         } else {
134             $this->message = "This commit message intentionally left blank.";
135         }
136     }
137
138     public function getVc()
139     {
140         return new \VersionControl_Git($this->gitDir);
141     }
142
143     /**
144      * Loads the list of files in this repository
145      *
146      * @return File[] Array of file objects
147      */
148     public function getFiles()
149     {
150         $files = $this->getFilePaths();
151         $arFiles = array();
152         foreach ($files as $name) {
153             $arFiles[] = new File($name, $this);
154         }
155         return $arFiles;
156     }
157
158     /**
159      * Decodes unicode characters in git filenames
160      * They begin and end with double quote characters, and may contain
161      * backslash + 3 letter octal code numbers representing the character.
162      *
163      * For example,
164      * > "t\303\244st.txt"
165      * means
166      * > täst.txt
167      *
168      * On the shell, you can pipe them into "printf" and have them decoded.
169      *
170      * @param string Encoded git file name
171      *
172      * @return string Decoded file name
173      */
174     protected function decodeFileName($name)
175     {
176         $name = substr($name, 1, -1);
177         $name = str_replace('\"', '"', $name);
178         $name = preg_replace_callback(
179             '#\\\\[0-7]{3}#',
180             function ($ar) {
181                 return chr(octdec(substr($ar[0], 1)));
182             },
183             $name
184         );
185         return $name;
186     }
187
188     /**
189      * Return array with all file paths in this repository
190      *
191      * @return array
192      */
193     protected function getFilePaths()
194     {
195         if ($this->hash === null) {
196             $hash = 'HEAD';
197         } else {
198             $hash = $this->hash;
199         }
200         $output = $this->getVc()->getCommand('ls-tree')
201             ->setOption('r')
202             ->setOption('name-only')
203             ->addArgument($hash)
204             ->execute();
205         $files = explode("\n", trim($output));
206         foreach ($files as &$file) {
207             if ($file{0} == '"') {
208                 $file = $this->decodeFileName($file);
209             }
210         }
211         return $files;
212     }
213
214     public function getFileByName($name, $bHasToExist = true)
215     {
216         $name = Tools::sanitizeFilename($name);
217         if ($name == '') {
218             throw new Exception_Input('Empty file name given');
219         }
220
221         if ($bHasToExist) {
222             $files = $this->getFilePaths();
223             if (array_search($name, $files) === false) {
224                 throw new Exception_Input('File does not exist');
225             }
226         }
227         return new File($name, $this);
228     }
229
230     public function hasFile($name)
231     {
232         try {
233             $this->getFileByName($name);
234         } catch (Exception $e) {
235             return false;
236         }
237         return true;
238     }
239
240     /**
241      * Permanently deletes the paste repository without any way to get
242      * it back.
243      *
244      * @return boolean True if all went well, false if not
245      */
246     public function delete()
247     {
248         $db = new Database();
249         $db->getIndexer()->deleteRepo($this);
250
251         $bOk = Tools::recursiveDelete($this->workDir)
252             && Tools::recursiveDelete($this->gitDir);
253
254         $not = new Notificator();
255         $not->delete($this);
256
257         return $bOk;
258     }
259
260     public function getTitle()
261     {
262         $desc = $this->getDescription();
263         if (trim($desc) != '') {
264             return $desc;
265         }
266
267         return 'paste #' . $this->id;
268     }
269
270     public function getDescription()
271     {
272         if (!is_readable($this->gitDir . '/description')) {
273             return null;
274         }
275         return file_get_contents($this->gitDir . '/description');
276     }
277
278     public function setDescription($description)
279     {
280         file_put_contents($this->gitDir . '/description', $description);
281     }
282
283     /**
284      * @return array Array with keys "email" and "name"
285      */
286     public function getOwner()
287     {
288         try {
289             $name = $this->getVc()->getCommand('config')
290                 ->addArgument('owner.name')->execute();
291         } catch (\VersionControl_Git_Exception $e) {
292             $name = $GLOBALS['phorkie']['auth']['anonymousName'];
293         }
294         try {
295             $email = $this->getVc()->getCommand('config')
296                 ->addArgument('owner.email')->execute();
297         } catch (\VersionControl_Git_Exception $e) {
298             $email = $GLOBALS['phorkie']['auth']['anonymousEmail'];
299         }
300
301         return array('name' => trim($name), 'email' => trim($email));
302     }
303
304     /**
305      * Get a link to the repository
306      *
307      * @param string  $type   Link type. Supported are:
308      *                        - "edit"
309      *                        - "delete"
310      *                        - "delete-confirm"
311      *                        - "display"
312      *                        - "embed"
313      *                        - "fork"
314      *                        - "revision"
315      * @param string  $option Additional link option, e.g. revision number
316      * @param boolean $full   Return full URL or normal relative
317      *
318      * @return string
319      */
320     public function getLink($type, $option = null, $full = false)
321     {
322         if ($type == 'edit') {
323             $link = $this->id . '/edit';
324             if ($option !== null) {
325                 $link .= '/' . urlencode($option);
326             }
327         } else if ($type == 'display') {
328             $link = $this->id;
329         } else if ($type == 'fork') {
330             $link = $this->id . '/fork';
331         } else if ($type == 'doap') {
332             $link = $this->id . '/doap';
333         } else if ($type == 'delete') {
334             $link = $this->id . '/delete';
335         } else if ($type == 'delete-confirm') {
336             $link = $this->id . '/delete/confirm';
337         } else if ($type == 'embed') {
338             $link = $this->id . '/embed';
339         } else if ($type == 'oembed-json') {
340             $link = 'oembed.php?format=json&url='
341                 . urlencode($this->getLink('display', null, true));
342         } else if ($type == 'oembed-xml') {
343             $link = 'oembed.php?format=xml&url='
344                 . urlencode($this->getLink('display', null, true));
345         } else if ($type == 'remotefork') {
346             return 'web+fork:' . $this->getLink('display', null, true);
347         } else if ($type == 'revision') {
348             $link = $this->id . '/rev/' . $option;
349         } else if ($type == 'linkback') {
350             $link = $this->id . '/linkback';
351         } else {
352             throw new Exception('Unknown link type');
353         }
354
355         if ($full) {
356             $link = Tools::fullUrl($link);
357         }
358         return $link;
359     }
360
361     public function getCloneURL($public = true)
362     {
363         $var = $public ? 'public' : 'private';
364         if (isset($GLOBALS['phorkie']['cfg']['git'][$var])) {
365             return $GLOBALS['phorkie']['cfg']['git'][$var] . $this->id . '.git';
366         }
367         return null;
368     }
369
370     /**
371      * Returns the history of the repository.
372      * We don't use VersionControl_Git's rev list fetcher since it does not
373      * give us separate email addresses and names, and it does not give us
374      * the amount of changed (added/deleted) lines.
375      *
376      * @return array Array of history objects
377      */
378     public function getHistory()
379     {
380         $output = $this->getVc()->getCommand('log')
381             ->setOption('pretty', 'format:commit %H%n%at%n%an%n%ae')
382             ->setOption('max-count', 10)
383             ->setOption('shortstat')
384             ->execute();
385
386         $arCommits = array();
387         $arOutput = explode("\n", $output);
388         $lines = count($arOutput);
389         $current = 0;
390         while ($current < $lines) {
391             $commit = new Repository_Commit();
392             list($name,$commit->hash) = explode(' ', $arOutput[$current]);
393             if ($name !== 'commit') {
394                 throw new Exception(
395                     'Git log output format not as expected: ' . $arOutput[$current]
396                 );
397             }
398             $commit->committerTime  = $arOutput[$current + 1];
399             $commit->committerName  = $arOutput[$current + 2];
400             $commit->committerEmail = $arOutput[$current + 3];
401
402             if (substr($arOutput[$current + 4], 0, 1) != ' ') {
403                 //commit without changed lines
404                 $arCommits[] = $commit;
405                 $current += 4;
406                 continue;
407             }
408
409             $arLineParts = explode(' ', trim($arOutput[$current + 4]));
410             $commit->filesChanged = $arLineParts[0];
411             $commit->linesAdded   = $arLineParts[3];
412             if (isset($arLineParts[5])) {
413                 $commit->linesDeleted = $arLineParts[5];
414             }
415
416             $current += 6;
417
418             $arCommits[] = $commit;
419         }
420
421         return $arCommits;
422     }
423
424     /**
425      * @return Repository_ConnectionInfo
426      */
427     public function getConnectionInfo()
428     {
429         return new Repository_ConnectionInfo($this);
430     }
431 }
432
433 ?>