/** * @class Scm.Git.Repo * * @extends Scm.Repo * * * */ static GitRepo _GitRepo; public class GitRepo : Object { public Gee.ArrayList cmds; public string name; public string gitdir; public string git_working_dir; public bool debug = false; public bool has_local_changes = false; public string git_status; public string git_diff; public string ahead_or_behind = ""; public Gee.HashMap ignore_files; public GitBranch currentBranch; public Gee.HashMap branches; // accessed in GitBranch.. public RooTicket? activeTicket; public Gee.HashMap cache; public static GitRepo singleton() { if (_GitRepo == null) { _GitRepo = new GitRepo.single(); _GitRepo.cache = new Gee.HashMap(); } return _GitRepo; } /** * index of.. matching gitpath.. */ public static int indexOf( Array repos, string gitpath) { // make a fake object to compare against.. var test_repo = GitRepo.get(gitpath); for(var i =0; i < repos.length; i++) { if (repos.index(i).gitdir == test_repo.gitdir) { return i; } } return -1; } public static Array list() { //if (GitRepo.list_cache != null) { // unowned Array ret = GitRepo.list_cache; // return ret; //} var cache = GitRepo.singleton().cache; var list_cache = new Array(); var dir = Environment.get_home_dir() + "/gitlive"; var f = File.new_for_path(dir); FileEnumerator file_enum; try { file_enum = f.enumerate_children( FileAttribute.STANDARD_DISPLAY_NAME + ","+ FileAttribute.STANDARD_TYPE, FileQueryInfoFlags.NONE, null); } catch (Error e) { return list_cache; } FileInfo next_file; while (true) { try { next_file = file_enum.next_file(null); if (next_file == null) { break; } } catch (Error e) { GLib.debug("Error: %s",e.message); break; } //print("got a file " + next_file.sudo () + '?=' + Gio.FileType.DIRECTORY); if (next_file.get_file_type() != FileType.DIRECTORY) { next_file = null; continue; } if (next_file.get_file_type() == FileType.SYMBOLIC_LINK) { next_file = null; continue; } if (next_file.get_display_name()[0] == '.') { next_file = null; continue; } var sp = dir+"/"+next_file.get_display_name(); var gitdir = dir + "/" + next_file.get_display_name() + "/.git"; if (!FileUtils.test(gitdir, FileTest.IS_DIR)) { continue; } var rep = GitRepo.get( sp ); list_cache.append_val(rep); } return list_cache; } public static GitRepo get(string path) { var cache = GitRepo.singleton().cache; if (cache.has_key(path)) { return cache.get(path); } return new GitRepo(path); } private GitRepo.single() { // used to create the signleton } /** * constructor: * * @param {Object} cfg - Configuration * (basically repopath is currently only critical one.) * */ private GitRepo(string path) { // cal parent? this.name = File.new_for_path(path).get_basename(); this.ignore_files = new Gee.HashMap(); this.git_working_dir = path; this.gitdir = path + "/.git"; if (!FileUtils.test(this.gitdir , FileTest.IS_DIR)) { this.gitdir = path; // naked... } this.cmds = new Gee.ArrayList (); var cache = GitRepo.singleton().cache; //Repo.superclass.constructor.call(this,cfg); if ( !cache.has_key(path) ) { cache.set( path, this); } this.loadBranches(); this.loadActiveTicket(); this.loadStatus(); } public bool is_wip_branch() { return this.currentBranch.name.has_prefix("wip_"); } public bool is_managed() { // is it a roojs origin? var r = this.git({ "remote" , "get-url" , "--push" , "origin"}); var uri = new Soup.URI(r); if (uri.get_host() != "git.roojs.com") { // we can only push to this url. -- unless we have forced it to be managed. return FileUtils.test(this.gitdir + "/.gitlive-managed" , FileTest.EXISTS); } // otherwise see if unmanaged is set to disable it.. return !FileUtils.test(this.gitdir + "/.gitlive-unmanaged" , FileTest.EXISTS); } public bool is_autocommit () { return !FileUtils.test(this.gitdir + "/.gitlive-disable-autocommit" , FileTest.EXISTS); } public void set_autocommit(bool val) { var cur = this.is_autocommit(); GLib.debug("SET auto commit : %s <= %s", val ? "ON" : "OFF", cur ? "ON" : "OFF"); if (cur == val) { return; // no change.. } if (!val) { FileUtils.set_contents(this.gitdir + "/.gitlive-disable-autocommit" , "x"); } else { // it exists... FileUtils.remove(this.gitdir + "/.gitlive-disable-autocommit" ); } } public bool is_auto_branch () { return FileUtils.test(this.gitdir + "/.gitlive-enable-auto-branch" , FileTest.EXISTS); } public void set_auto_branch(bool val) { var cur = this.is_auto_branch(); GLib.debug("SET auto branch : %s <= %s", val ? "ON" : "OFF", cur ? "ON" : "OFF"); if (cur == val) { return; // no change.. } if (val) { FileUtils.set_contents(this.gitdir + "/.gitlive-enable-auto-branch" , "x"); } else { // it exists... FileUtils.remove(this.gitdir + "/.gitlive-enable-auto-branch" ); } } public bool is_autopush () { return !FileUtils.test(this.gitdir + "/.gitlive-disable-autopush" , FileTest.EXISTS); } public void set_autopush(bool val) { var cur = this.is_autopush(); GLib.debug("SET auto push : %s <= %s", val ? "ON" : "OFF", cur ? "ON" : "OFF"); if (cur == val) { return; // no change.. } if (!val) { FileUtils.set_contents(this.gitdir + "/.gitlive-disable-autopush" , ""); } else { // it exists... FileUtils.remove(this.gitdir + "/.gitlive-disable-autopush" ); } } public void loadStatus() { var r = this.git({ "status" , "--porcelain" }); this.git_status = r; this.has_local_changes = r.length > 0; var rs = this.git({ "status" , "-sb" }); this.ahead_or_behind = rs.contains("[ahead") ? "A" : (rs.contains("[behind") ? "B" : ""); this.git_diff = this.git({ "diff" , "HEAD", "--no-color" }); } public void loadBranches() { GitBranch.loadBranches(this); } public string branchesToString() { var ret = ""; foreach( var br in this.branches.values) { if (br.name == "") { continue; } ret += ret.length > 0 ? "\n" : ""; ret += br.name; } return ret; } public static void doMerges(string action, string ticket_id, string commit_message) { GitMonitor.gitmonitor.stop(); var commitrevs = ""; var sucess = true; foreach(var repo in GitRepo.singleton().cache.values) { if (repo.activeTicket != null && repo.activeTicket.id == ticket_id) { var res = repo.doMerge(action,ticket_id, commit_message); if (!res) { sucess = false; continue; } commitrevs += commitrevs.length > 0 ? " " : ""; commitrevs += repo.currentBranch.lastrev; } } if (sucess && action == "CLOSE") { RooTicket.singleton().getById(ticket_id).close(commitrevs); } GitMonitor.gitmonitor.start(); } public bool doMerge(string action, string ticket_id, string commit_message) { // in theory we should check to see if other repo's have got the same branch and merge all them at the same time. // also need to decide which branch we will merge into? var ret = ""; if (action == "CLOSE" || action == "LEAVE") { try { var oldbranch = this.currentBranch.name; this.setActiveTicket(null, "master"); string [] cmd = { "merge", "--squash", oldbranch }; this.git( cmd ); cmd = { "commit", "-a" , "-m", commit_message }; this.git( cmd ); this.push(); this.loadBranches(); // updates lastrev.. var notification = new Notify.Notification( "Merged branch %s to master".printf(oldbranch), "", "dialog-information" ); notification.set_timeout(5); notification.show(); // close ticket.. return true; } catch (Error e) { GitMonitor.gitmonitor.pauseError(e.message); return false; } // error~?? -- show the error dialog... return false; } if (action == "MASTER") { // merge master into ours.. try { string[] cmd = { "merge", "master" }; this.git( cmd ); var notification = new Notify.Notification( "Merged code from master to %s".printf(this.currentBranch.name), "", "dialog-information" ); notification.set_timeout(5); notification.show(); return true; } catch (Error e) { GitMonitor.gitmonitor.pauseError(e.message); return false; } } if (action == "EXIT") { try { var oldbranch = this.currentBranch.name; this.setActiveTicket(null, "master"); this.loadBranches(); var notification = new Notify.Notification( "Left branch %s".printf(oldbranch), "", "dialog-information" ); notification.set_timeout(5); notification.show(); return true; } catch (Error e) { GitMonitor.gitmonitor.pauseError(e.message); return false; } // error~?? -- show the error dialog... } return false; } public void loadActiveTicket() { this.activeTicket = null; if (!FileUtils.test(this.gitdir + "/.gitlive-active-ticket" , FileTest.EXISTS)) { return; } string ticket_id; FileUtils.get_contents(this.gitdir + "/.gitlive-active-ticket" , out ticket_id); if (ticket_id.length < 1) { return; } this.activeTicket = RooTicket.singleton().getById(ticket_id.strip()); } public bool setActiveTicket(RooTicket? ticket, string branchname) { if (!this.createBranchNamed(branchname)) { return false; } if (ticket != null) { FileUtils.set_contents(this.gitdir + "/.gitlive-active-ticket" , ticket.id); } else { FileUtils.remove(this.gitdir + "/.gitlive-active-ticket" ); } this.activeTicket = ticket; return true; } public bool createBranchNamed(string branchname) { if (this.branches.has_key(branchname)) { this.switchToExistingBranchNamed(branchname); } else { this.createNewBranchNamed(branchname); } var notification = new Notify.Notification( "Changed to branch %s".printf(branchname), "", "dialog-information" ); notification.set_timeout(5); notification.show(); this.loadBranches(); // update branch list... //GitMonitor.gitmonitor.runQueue(); // no point - we have hidden the queue.. return true; } bool switchToExistingBranchNamed(string branchname) { var stash = false; // this is where it get's tricky... string files = ""; try { string[] cmd = { "ls-files" , "-m" }; // list the modified files.. files = this.git(cmd); stash = files.length> 1 ; cmd = { "stash" }; if (stash) { this.git(cmd); } this.pull(); cmd = { "checkout", branchname }; this.git(cmd); } catch(Error e) { GitMonitor.gitmonitor.pauseError(e.message); return false; } try { if (branchname != "master") { string[] cmd = { "merge", "master" }; this.git(cmd); this.push(); } } catch(Error e) { string[] cmd = { "checkout", "master" }; this.git(cmd); GitMonitor.gitmonitor.pauseError( "Use\n\na) git checkout %s\nb) git mergetool\nc) git commit\nd) git push\n d) stash pop \ne) start gitlive again\n".printf( branchname) + e.message ); return false; } try { string[] cmd = { "stash", "pop" }; if (stash) { this.git(cmd); var fl = files.split("\n"); cmd = { "commit", "-m" , "Changed " + string.joinv("",fl) }; foreach(var f in fl) { if (f.length < 1) continue; cmd += f; } this.git(cmd); } } catch(Error ee) { GitMonitor.gitmonitor.pauseError(ee.message); return false; } this.push(); return true; } bool createNewBranchNamed(string branchname) { var stash = false; try { string[] cmd = { "ls-files" , "-m" }; // list the modified files.. var files = this.git(cmd); stash = files.length> 1 ; cmd = { "checkout", "-b" , branchname }; this.git(cmd); cmd = { "push", "-u" , "origin" ,"HEAD" }; this.git(cmd); if (stash) { var fl = files.split("\n"); cmd = { "commit", "-m" , "Changed " + string.joinv("",fl) }; foreach(var f in fl) { if (f.length < 1) continue; cmd += f; } this.git(cmd); this.push(); } } catch(Error ee) { GitMonitor.gitmonitor.pauseError(ee.message); return false; } return true; } /** * add: * add files to track. * * @argument {Array} files the files to add. */ public string add ( Gee.ArrayList files ) throws Error, SpawnError { // should really find out if these are untracked files each.. // we run multiple versions to make sure that if one failes, it does not ignore the whole lot.. // not sure if that is how git works.. but just be certian. var ret = ""; for (var i = 0; i < files.size;i++) { var f = files.get(i).vname; try { string[] cmd = { "add", f }; this.git( cmd ); } catch (Error e) { ret += e.message + "\n"; } } return ret; } public bool is_ignore(string fname) throws Error, SpawnError { if (fname == ".gitignore") { this.ignore_files.clear(); } if (this.ignore_files.has_key(fname)) { return this.ignore_files.get(fname); } try { var ret = this.git( { "check-ignore" , fname } ); this.ignore_files.set(fname, ret.length > 0); return ret.length > 0; } catch (SpawnError e) { this.ignore_files.set(fname, false); return false; } } /** * remove: * remove files to track. * * @argument {Array} files the files to add. */ public string remove ( Gee.ArrayList files ) throws Error, SpawnError { // this may fail if files do not exist.. // should really find out if these are untracked files each.. // we run multiple versions to make sure that if one failes, it does not ignore the whole lot.. // not sure if that is how git works.. but just be certian. var ret = ""; for (var i = 0; i < files.size;i++) { var f = files.get(i).vname; try { string[] cmd = { "rm", "-f" , f }; this.git( cmd ); } catch (Error e) { ret += e.message + "\n"; } } return ret; } /** * commit: * perform a commit. * * @argument {Object} cfg commit configuration * * @property {String} name (optional) * @property {String} email (optional) * @property {String} changed (date) (optional) * @property {String} reason (optional) * @property {Array} files - the files that have changed. * */ public string commit ( string message, Gee.ArrayList files ) throws Error, SpawnError { /* var env = []; if (typeof(cfg.name) != 'undefined') { args.push( { 'author' : cfg.name + ' <' + cfg.email + '>' }); env.push( "GIT_COMMITTER_NAME" + cfg.name, "GIT_COMMITTER_EMAIL" + cfg.email ); } if (typeof(cfg.changed) != 'undefined') { env.push("GIT_AUTHOR_DATE= " + cfg.changed ) } */ string[] args = { "commit", "-m" }; args += (message.length > 0 ? message : "Changed" ); for (var i = 0; i< files.size ; i++ ) { args += files.get(i).vname; // full path? } return this.git(args); } /** * pull: * Fetch and merge remote repo changes into current branch.. * * At present we just need this to update the current working branch.. * -- maybe later it will have a few options and do more stuff.. * */ public string pull () throws Error, SpawnError { // should probably hand error conditions better... string[] cmd = { "pull" , "--no-edit" }; return this.git( cmd ); } public delegate void GitAsyncCallback (GitRepo repo, int err, string str); public void pull_async(GitAsyncCallback cb) { string[] cmd = { "pull" , "--no-edit" }; this.git_async( cmd , cb); } /** * push: * Send local changes to remote repo(s) * * At present we just need this to push the current branch. * -- maybe later it will have a few options and do more stuff.. * */ public string push () throws Error, SpawnError { // should return this.git({ "push" }); } /** * git: * The meaty part.. run spawn.. with git.. * * */ public string git(string[] args_in ) throws Error, SpawnError { // convert arguments. string[] args = { "git" }; //args += "--git-dir"; //args += this.gitdir; args += "--no-pager"; //if (this.gitdir != this.repopath) { // args += "--work-tree"; // args += this.repopath; //} for (var i = 0; i < args_in.length;i++) { args += args_in[i]; } //this.lastCmd = args.join(" "); //if(this.debug) { GLib.debug( "CWD=%s", this.git_working_dir ); GLib.debug( "cmd: %s", string.joinv (" ", args)); //} string[] env = {}; string home = "HOME=" + Environment.get_home_dir() ; env += home ; // do not need to set gitpath.. //if (File.exists(this.repo + '/.git/config')) { //env.push("GITPATH=" + this.repo ); //} var cfg = new SpawnConfig(this.git_working_dir , args , env); //cfg.debug = true; // may throw error... var sp = new Spawn(cfg); // diff output is a bit big.. if (args_in[0] != "diff") { GLib.debug( "GOT: %s" , sp.output); } // parse output for some commands ? return sp.output; } unowned GitAsyncCallback git_async_on_callback; public void git_async( string[] args_in, GitAsyncCallback cb ) throws Error, SpawnError { // convert arguments. this.git_async_on_callback = cb; string[] args = { "git" }; //args += "--git-dir"; //args += this.gitdir; args += "--no-pager"; //if (this.gitdir != this.repopath) { // args += "--work-tree"; // args += this.repopath; //} for (var i = 0; i < args_in.length;i++) { args += args_in[i]; } //this.lastCmd = args.join(" "); //if(this.debug) { GLib.debug( "CWD=%s", this.git_working_dir ); //print( "cmd: %s\n", string.joinv (" ", args)); //} string[] env = {}; string home = "HOME=" + Environment.get_home_dir() ; env += home ; // do not need to set gitpath.. //if (File.exists(this.repo + '/.git/config')) { //env.push("GITPATH=" + this.repo ); //} var cfg = new SpawnConfig(this.git_working_dir , args , env); cfg.async = true; // may throw error... var sp = new Spawn(cfg); //sp.ref(); //this.ref(); sp.run(this.git_async_on_complete); } void git_async_on_complete(int err, string output) { GLib.debug("GOT %d : %s", err, output); this.git_async_on_callback(this, err, output); // this.unref(); // sp.unref(); } public void update_async(GitAsyncCallback cb) { string[] cmd = { "fetch" , "--all" }; this.git_async( cmd , cb); } static uint update_all_total = 0; static string update_all_after = ""; public static void updateAll(string after) { update_all_after = after; var tr = GitRepo.singleton().cache; update_all_total = tr.size; foreach(var repo in tr.values) { if (!repo.is_managed()) { update_all_total--; continue; } repo.update_async(updateAllCallback); } } public static void updateAllCallback(GitRepo repo, int err, string res) { repo.loadBranches(); repo.loadStatus(); update_all_total--; if (update_all_total > 0 ) { return; } switch (update_all_after) { case "show_clones": Clones.singleton().show(); break; default: break; } return; } }