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