Updated ChangeLog and README.rst
[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     public function loadMessage()
108     {
109         $rev = (isset($this->hash)) ? $this->hash : 'HEAD';
110         $output = $this->getVc()->getCommand('log')
111             ->setOption('oneline')
112             ->addArgument('-1')
113             ->addArgument($rev)
114             ->execute();
115         $output = trim($output);
116         if (strpos($output, ' ') > 0) {
117             $output = substr($output, strpos($output, ' '), strlen($output));
118             $this->message = trim($output);
119         } else {
120             $this->message = "This commit message intentionally left blank.";
121         }
122     }
123
124     public function loadById($id)
125     {
126         if (!is_numeric($id)) {
127             throw new Exception_Input('Paste ID not numeric');
128         }
129         $this->id = (int)$id;
130         $this->loadDirs();
131         $this->loadHash();
132     }
133
134     public function getVc()
135     {
136         return new \VersionControl_Git($this->gitDir);
137     }
138
139     /**
140      * Loads the list of files in this repository
141      *
142      * @return File[] Array of files
143      */
144     public function getFiles()
145     {
146         $files = $this->getFilePaths();
147         $arFiles = array();
148         foreach ($files as $name) {
149             $arFiles[] = new File($name, $this);
150         }
151         return $arFiles;
152     }
153
154     protected function getFilePaths()
155     {
156         if ($this->hash === null) {
157             $hash = 'HEAD';
158         } else {
159             $hash = $this->hash;
160         }
161         $output = $this->getVc()->getCommand('ls-tree')
162             ->setOption('r')
163             ->setOption('name-only')
164             ->addArgument($hash)
165             ->execute();
166         return explode("\n", trim($output));
167     }
168
169     public function getFileByName($name, $bHasToExist = true)
170     {
171         $name = Tools::sanitizeFilename($name);
172         if ($name == '') {
173             throw new Exception_Input('Empty file name given');
174         }
175
176         if ($bHasToExist) {
177             $files = $this->getFilePaths();
178             if (array_search($name, $files) === false) {
179                 throw new Exception_Input('File does not exist');
180             }
181         }
182         return new File($name, $this);
183     }
184
185     public function hasFile($name)
186     {
187         try {
188             $this->getFileByName($name);
189         } catch (Exception $e) {
190             return false;
191         }
192         return true;
193     }
194
195     /**
196      * Permanently deletes the paste repository without any way to get
197      * it back.
198      *
199      * @return boolean True if all went well, false if not
200      */
201     public function delete()
202     {
203         $db = new Database();
204         $db->getIndexer()->deleteRepo($this);
205
206         return Tools::recursiveDelete($this->workDir)
207             && Tools::recursiveDelete($this->gitDir);
208     }
209
210     public function getTitle()
211     {
212         $desc = $this->getDescription();
213         if (trim($desc) != '') {
214             return $desc;
215         }
216
217         return 'paste #' . $this->id;
218     }
219
220     public function getDescription()
221     {
222         if (!is_readable($this->gitDir . '/description')) {
223             return null;
224         }
225         return file_get_contents($this->gitDir . '/description');
226     }
227
228     public function setDescription($description)
229     {
230         file_put_contents($this->gitDir . '/description', $description);
231     }
232
233     /**
234      * Get a link to the repository
235      *
236      * @param string $type Link type. Supported are:
237      *                     - "edit"
238      *                     - "delete"
239      *                     - "delete-confirm"
240      *                     - "display"
241      *                     - "fork"
242      *                     - "revision"
243      * @param string $option
244      *
245      * @return string
246      */
247     public function getLink($type, $option = null)
248     {
249         if ($type == 'edit') {
250             return '/' . $this->id . '/edit';
251         } else if ($type == 'display') {
252             return '/' . $this->id;
253         } else if ($type == 'fork') {
254             return '/' . $this->id . '/fork';
255         } else if ($type == 'delete') {
256             return '/' . $this->id . '/delete';
257         } else if ($type == 'delete-confirm') {
258             return '/' . $this->id . '/delete/confirm';
259         } else if ($type == 'revision') {
260             return '/' . $this->id . '/rev/' . $option;
261         }
262         throw new Exception('Unknown link type');
263     }
264
265     public function getCloneURL($public = true)
266     {
267         $var = $public ? 'public' : 'private';
268         if (isset($GLOBALS['phorkie']['cfg']['git'][$var])) {
269             return $GLOBALS['phorkie']['cfg']['git'][$var] . $this->id . '.git';
270         }
271         return null;
272     }
273
274     /**
275      * Returns the history of the repository.
276      * We don't use VersionControl_Git's rev list fetcher since it does not
277      * give us separate email addresses and names, and it does not give us
278      * the amount of changed (added/deleted) lines.
279      *
280      * @return array Array of history objects
281      */
282     public function getHistory()
283     {
284         $output = $this->getVc()->getCommand('log')
285             ->setOption('pretty', 'format:commit %H%n%at%n%an%n%ae')
286             ->setOption('max-count', 10)
287             ->setOption('shortstat')
288             ->execute();
289
290         $arCommits = array();
291         $arOutput = explode("\n", $output);
292         $lines = count($arOutput);
293         $current = 0;
294         while ($current < $lines) {
295             $commit = new Repository_Commit();
296             list($name,$commit->hash) = explode(' ', $arOutput[$current]);
297             if ($name !== 'commit') {
298                 throw new Exception(
299                     'Git log output format not as expected: ' . $arOutput[$current]
300                 );
301             }
302             $commit->committerTime  = $arOutput[$current + 1];
303             $commit->committerName  = $arOutput[$current + 2];
304             $commit->committerEmail = $arOutput[$current + 3];
305
306             $arLineParts = explode(' ', trim($arOutput[$current + 4]));
307             $commit->filesChanged = $arLineParts[0];
308             $commit->linesAdded   = $arLineParts[3];
309             if (isset($arLineParts[5])) {
310                 $commit->linesDeleted = $arLineParts[5];
311             }
312
313             $current += 6;
314
315             $arCommits[] = $commit;
316         }
317
318         return $arCommits;
319     }
320 }
321
322 ?>