2c429f96ed1a047e76a7f5c0f42739f078984cd9
[GITManagedWebpage.git] / GITManagedWebpage.class.php
1 <?php
2 /*************************** GITManagedWebpage **************************
3  * Copyright (C) 2013       Philipp Kreil (pk910)                       *
4  *                                                                      *
5  * This program is free software: you can redistribute it and/or modify *
6  * it under the terms of the GNU General Public License as published by *
7  * the Free Software Foundation, either version 3 of the License, or    *
8  * (at your option) any later version.                                  *
9  *                                                                      *
10  * This program is distributed in the hope that it will be useful,      *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of       *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        *
13  * GNU General Public License for more details.                         *
14  *                                                                      *
15  * You should have received a copy of the GNU General Public License    *
16  * along with this program. If not, see <http://www.gnu.org/licenses/>. *
17  *                                                                      *
18  ************************************************************************
19  *
20  *  GITManagedWebpage.class.php
21  *
22  * Functions to manage a Website using GIT Version Control
23  *
24  */
25
26 class GITManagedWebpage {
27     const ERROR_CRITICAL = 1;
28     const SESSION_PREFIX = "GITManagedWebpage_";
29
30     private $giturl;
31     private $workdir, $localdir;
32     private $ready = false;
33     private $loopedcall = false;
34     private $config = null;
35     private $config_changed = true;
36     private $activeSession = null;
37     
38     public function __construct($giturl, $workdir = null, $localdir = null) {
39         if(session_status() != PHP_SESSION_ACTIVE) {
40             session_start();
41         }
42         
43         if(defined("GITMANAGED_EXECUTED")) {
44             $this->loopedcall = true;
45             return;
46         }
47         
48         if($workdir === null) {
49             $workdir = dirname(__FILE__);
50             if(substr($workdir, -1) != "/")
51                 $workdir .= "/";
52             $workdir .= ".gitmanaged/";
53         }
54         else if(substr($workdir, -1) != "/")
55             $workdir .= "/";
56         
57         if($localdir === null) {
58             $localdir = dirname(__FILE__);
59             if(substr($localdir, -1) != "/")
60                 $localdir .= "/";
61         }
62         else if(substr($localdir, -1) != "/")
63             $localdir .= "/";
64         
65         $this->giturl = $giturl;
66         $this->workdir = $workdir;
67         $this->localdir = $localdir;
68         
69         if(!file_exists($this->workdir) || !is_dir($this->workdir)) {
70             if(file_exists($this->workdir) && !is_dir($this->workdir)) {
71                 $this->error(self::ERROR_CRITICAL, "local workdir (".htmlspecialchars($this->workdir).") is not a directory.");
72                 return;
73             }
74             $this->setupWorkdir();
75         }
76         $this->ready = true;
77     }
78     
79     /* private function gitcmd(...)
80     * Execute a git command and return output
81     */
82     private function gitcmd() {
83         $args = func_get_args();
84         $argstr = "";
85         foreach($args as $arg) {
86             if(is_array($arg)) {
87                 foreach($arg as $subarg) {
88                     $argstr .= " ".escapeshellarg($subarg);
89                 }
90             } else
91                 $argstr .= " ".escapeshellarg($arg);
92         }
93         $gitcmd = 'git '.escapeshellarg('--git-dir='.$this->workdir.'repository/.git').' '.escapeshellarg('--work-tree='.$this->workdir.'repository').$argstr;
94         $output = shell_exec($gitcmd);
95         return $output;
96     }
97     
98     /* private function setConfig($name, $value)
99     * store a option in the configuration
100     */
101     private function setConfig($name, $value) {
102         $this->config[strtolower($name)] = $value;
103         $this->config_changed = true;
104     }
105     
106     /* private function getConfig($name)
107     * get an option from the configuration
108     */
109     private function getConfig($name) {
110         if($this->config == null) {
111             if(!$this->ready)
112                 return null;
113             if(file_exists($this->workdir."config.txt")) {
114                 $config_txt = @file_get_contents($this->workdir."config.txt");
115                 $this->config = unserialize($config_txt);
116             } else {
117                 $this->config = array();
118                 return null;
119             }
120         }
121         if(array_key_exists(strtolower($name), $this->config))
122             return $this->config[strtolower($name)];
123         else
124             return null;
125     }
126     
127     private function saveConfig() {
128         if($this->config_changed && $this->ready) {
129             $fp = fopen($this->workdir."config.txt", "w");
130             fwrite($fp, serialize($this->config));
131             fclose($fp);
132         }
133     }
134     
135     /* private function setupWorkdir()
136     * Setup local GITManagedWebpage Work directory with git repository
137     */
138     private function setupWorkdir() {
139         // check requirements
140         $git_exec = shell_exec('which git');
141         if(!preg_match('#git#', $git_exec)) {
142             $this->error(self::ERROR_CRITICAL, "git not installed locally.");
143             return;
144         }
145         
146         mkdir($this->workdir);
147         mkdir($this->workdir.'repository');
148         shell_exec('git clone '.escapeshellarg($this->giturl).' '.escapeshellarg($this->workdir.'repository'));
149         $gitok = $this->gitcmd("status");
150         if(preg_match("#Not a git repository#", $gitok)) {
151             rmdir($this->workdir.'repository');
152             rmdir($this->workdir);
153             $this->error(self::ERROR_CRITICAL, "error cloning git repository.");
154             return;
155         }
156         $this->ready = true;
157         
158         $default_branch = str_replace(array("\r", "\n"), array("", ""), $this->gitcmd("rev-parse", "--abbrev-ref", "HEAD"));
159         $this->setConfig("defaultbranch", $default_branch);
160         $this->saveConfig();
161     }
162     
163     private function getActiveBranch() {
164         if($this->activeSession)
165             return $this->activeSession;
166         else if(isset($_SESSION[self::SESSION_PREFIX.'branch']))
167             return $_SESSION[self::SESSION_PREFIX.'branch'];
168         else
169             return $this->getConfig("defaultbranch");
170     }
171     
172     private function setActiveBranch($branch, $remember) {
173         $this->activeSession = $branch;
174         if($remember)
175             $_SESSION[self::SESSION_PREFIX.'branch'] = $branch;
176     }
177     
178     private function getLocalUntrackedFiles() {
179         $default_branch = $this->getConfig("defaultbranch");
180         $tracked_files = $this->gitcmd("ls-tree", $default_branch, "--full-name", "--name-only");
181         $local_files = shell_exec("find ".escapeshellarg($this->localdir));
182         $untracked_files = array();
183         
184         $tracked_files = explode("\n", str_replace(array("\r"), array(""), $tracked_files));
185         $local_files = explode("\n", str_replace(array("\r"), array(""), $local_files));
186         
187         foreach($local_files as $local_file) {
188             if(!$local_file)
189                 continue;
190             if($strip_local || (($strip_local = strlen($this->localdir)) && substr($local_file, 0, $strip_local) == $this->localdir)) {
191                 $local_file = substr($local_file, $strip_local);
192             }
193             $tracked = false;
194             foreach($tracked_files as $tracked_file) {
195                 if($tracked_file == $local_file) {
196                     $tracked = true;
197                     break;
198                 }
199             }
200             if(!$tracked) {
201                 $untracked_files[] = $local_file;
202             }
203         }
204         return $untracked_files;
205     }
206     
207     private function branchExists($branch) {
208         //check if branch exists
209         $gitret = $this->gitcmd("rev-list", "--max-count=1", $branch);
210         if(!preg_match("#([a-z0-9]{40})#", $gitret, $match))
211             return false;
212         else
213             return $match[1];
214     }
215     
216     private function localBranchPath($branch, $create = false) {
217         $default_branch = $this->getConfig("defaultbranch");
218         if($branch == $default_branch)
219             $dir = $this->localdir;
220         else
221             $dir = $this->workdir.'branch_'.str_replace(array('/'), array('_'), $branch).'/';
222         if(file_exists($dir))
223             return $dir;
224         else if($create) {
225             mkdir($dir);
226             return $dir;
227         } else 
228             return false;
229     }
230     
231     private function updateBranch($branch, $path) {
232         if(substr($path, -1) != '/')
233             $path .= '/';
234         $current_branch = str_replace(array("\r", "\n"), array("", ""), $this->gitcmd("rev-parse", "--abbrev-ref", "HEAD"));
235         if($current_branch != $branch)
236             $this->gitcmd("checkout", $branch);
237         $this->gitcmd("pull");
238         $gitret = $this->gitcmd("rev-list", "--max-count=1", $branch);
239         preg_match("#([a-z0-9]{40})#", $gitret, $match);
240         $newest_version = $match[1];
241         
242         $deleted_files = array();
243         if(($current_version = $this->getConfig('version_'.$branch))) {
244             if($current_version == $newest_version)
245                 return;
246             else {
247                 $override_all = true;
248                 $delfiles = $this->gitcmd("diff", "--diff-filter=D", "--name-only", $current_version, $newest_version);
249                 $delfiles = explode("\n", str_replace(array("\r"), array(""), $delfiles));
250                 foreach($delfiles as $file) {
251                     if(!$file)
252                         continue;
253                     $deleted_files[] = $file;
254                 }
255             }
256         } else
257             $override_all = true;
258         if($override_all) {
259             $rsync_present = preg_match("#rsync#", `which rsync`);
260             if($rsync_present)
261                 shell_exec('rsync -avz --exclude ".git" '.escapeshellarg($this->workdir."repository/").' '.escapeshellarg($path));
262             else
263                 shell_exec('tar -c --exclude ".git" -C '.escapeshellarg($this->workdir."repository").' . | tar -x -C '.escapeshellarg($path));
264             
265             // remove deleted files
266             foreach($deleted_files as $file) {
267                 unlink($path.$file);
268             }
269         }
270         $this->setConfig('version_'.$branch, $newest_version);
271         $this->saveConfig();
272     }
273     
274     /* public function update()
275     * Pulls latest commit of active branch and overwrites files in branch folder
276     */
277     public function update() {
278         if($this->loopedcall)
279             return;
280         
281         $active_branch = $this->getActiveBranch();
282         
283         if(!$this->branchExists($active_branch))
284             return false;
285         $dir = $this->localBranchPath($active_branch, true);
286         $this->updateBranch($active_branch, $dir);
287     }
288     
289     public function setBranch($branch, $remember = false) {
290         if($this->loopedcall)
291             return;
292         
293         if(!$this->branchExists($branch)) {
294             if(!$this->branchExists('origin/'.$branch))
295                 return false;
296         }
297         $this->setActiveBranch($branch, $remember);
298         
299         if(!$this->localBranchPath($branch)) {
300             $dir = $this->localBranchPath($branch, true);
301             $this->updateBranch($branch, $dir);
302         }
303     }
304     
305     public function execute($file = null) {
306         if($this->loopedcall)
307             return;
308         define("GITMANAGED_EXECUTED", true);
309         
310         if(!$file)
311             $file = $_SERVER['PHP_SELF'];
312         if($file[0] == '/')
313             $file = substr($file, 1);
314         
315         $default_branch = $this->getConfig("defaultbranch");
316         $active_branch = $this->getActiveBranch();
317         if($active_branch != $default_branch) {
318             if(!($dir = $this->localBranchPath($branch))) {
319                 $dir = $this->localBranchPath($active_branch, true);
320                 $this->updateBranch($active_branch, $dir);
321             }
322             include_once($dir.$file);
323             die();
324         }
325     }
326     
327 }
328
329 ?>