def repolist(format): cfg = RepoListConfig() for info in cfg.find_all(): vars_ = dict(repoid=info.repoid, localpath=info.localrepo.repo_path, canonical=(info.canonicalrepo.repo_path if info.canonicalrepo else '')) print(format % vars_)
def update(identifiers, nopull, only): ''' Performs a `git pull` in each of the repositories registered with `homely add`, runs all of their HOMELY.py scripts, and then performs automatic cleanup as necessary. REPO This should be the path to a local dotfiles repository that has already been registered using `homely add`. If you specify one or more `REPO`s then only the HOMELY.py scripts from those repositories will be run, and automatic cleanup will not be performed (automatic cleanup is only possible when homely has done an update of all repositories in one go). If you do not specify a REPO, all repositories' HOMELY.py scripts will be run. The --nopull and --only options are useful when you are working on your HOMELY.py script - the --nopull option stops you from wasting time checking the internet for the same updates on every run, and the --only option allows you to execute only the section you are working on. ''' mkcfgdir() setallowpull(not nopull) cfg = RepoListConfig() if len(identifiers): updatedict = {} for identifier in identifiers: repo = cfg.find_by_any(identifier, "ilc") if repo is None: hint = "Try running %s add /path/to/this/repo first" % CMD raise Fatal("Unrecognised repo %s (%s)" % (identifier, hint)) updatedict[repo.repoid] = repo updatelist = updatedict.values() cleanup = len(updatelist) == cfg.repo_count() else: updatelist = list(cfg.find_all()) cleanup = True success = run_update(updatelist, pullfirst=not nopull, only=only, cancleanup=cleanup) if not success: sys.exit(1)
def forget(identifier): ''' Tells homely to forget about a dotfiles repository that was previously added. You can then run `homely update` to have homely perform automatic cleanup of anything that was installed by that dotfiles repo. REPO This should be the path to a local dotfiles repository that has already been registered using `homely add`. You may specify multiple REPOs to remove at once. ''' errors = False for one in identifier: cfg = RepoListConfig() info = cfg.find_by_any(one, "ilc") if not info: warn("No repos matching %r" % one) errors = True continue # update the config ... note("Removing record of repo [%s] at %s" % (info.shortid(), info.localrepo.repo_path)) with saveconfig(RepoListConfig()) as cfg: cfg.remove_repo(info.repoid) # if there were errors, then don't try and do an update if errors: sys.exit(1)
def add(repo_path, dest_path): ''' Registers a git repository with homely so that it will run its `HOMELY.py` script on each invocation of `homely update`. `homely add` also immediately executes a `homely update` so that the dotfiles are installed straight away. If the git repository is hosted online, a local clone will be created first. REPO_PATH A path to a local git repository, or the URL for a git repository hosted online. If REPO_PATH is a URL, then it should be in a format accepted by `git clone`. If REPO_PATH is a URL, you may also specify DEST_PATH. DEST_PATH If REPO_PATH is a URL, then the local clone will be created at DEST_PATH. If DEST_PATH is omitted then the path to the local clone will be automatically derived from REPO_PATH. ''' mkcfgdir() try: repo = getrepohandler(repo_path) except NotARepo as err: echo("ERROR: {}: {}".format(ERR_NOT_A_REPO, err.repo_path)) sys.exit(1) # if the repo isn't on disk yet, we'll need to make a local clone of it if repo.isremote: localrepo, needpull = addfromremote(repo, dest_path) elif dest_path: raise UsageError("DEST_PATH is only for repos hosted online") else: try: repoid = repo.getrepoid() except RepoHasNoCommitsError as err: echo("ERROR: {}".format(ERR_NO_COMMITS)) sys.exit(1) localrepo = RepoInfo(repo, repoid, None) needpull = False # if we don't have a local repo, then there is nothing more to do if not localrepo: return # remember this new local repo with saveconfig(RepoListConfig()) as cfg: cfg.add_repo(localrepo) success = run_update([localrepo], pullfirst=needpull, cancleanup=True) if not success: sys.exit(1)
def addfromremote(repo, dest_path): assert isinstance(repo, Repo) and repo.isremote rlist = RepoListConfig() if repo.iscanonical: # abort if we have already added this repo before match = rlist.find_by_canonical(repo.repo_path) if match: note("Repo [%s] from %s has already been added" % (match.shortid(), repo.repo_path)) return match, True # figure out where the temporary clone should be moved to after it is # created if dest_path is None: assert repo.suggestedlocal is not None dest_path = os.path.join(os.environ["HOME"], repo.suggestedlocal) with tmpdir(os.path.basename(dest_path)) as tmp: # clone the repo to a temporary location note("HOME: %s" % os.environ["HOME"]) note("tmp: %s" % tmp) note("Cloning %s to tmp:%s" % (repo.repo_path, tmp)) repo.clonetopath(tmp) # find out the first commit id localrepo = repo.frompath(tmp) assert isinstance(localrepo, repo.__class__) tmprepoid = localrepo.getrepoid() # if we recognise the repo, record the canonical path onto # the repo info so we don't have to download it again match = rlist.find_by_id(tmprepoid) if match is not None: note("Repo [%s] from has already been added" % match.localrepo.shortid(tmprepoid)) if repo.iscanonical: match.canonicalrepo = repo rlist.add_repo(match) rlist.writejson() return match, True if os.path.exists(dest_path): destrepo = localrepo.frompath(dest_path) if not destrepo: # TODO: use a different type of exception here raise Exception("DEST_PATH %s already exists" % dest_path) # check that the repo there is the right repo destid = destrepo.getrepoid() if destid != tmprepoid: # TODO: this should be a different type of exception raise Exception("Repo with id [%s] already exists at %s" % (destrepo.getrepoid(False), dest_path)) # we can use the repo that already exists at dest_path note("Using the existing repo [%s] at %s" % (destrepo.shortid(destid), dest_path)) return RepoInfo(destrepo, destid), True # move our temporary clone into the final destination os.rename(tmp, dest_path) destrepo = localrepo.frompath(dest_path) assert destrepo is not None info = RepoInfo( destrepo, destrepo.getrepoid(), repo if repo.iscanonical else None, ) return info, False
def run_update(infos, pullfirst, only=None, cancleanup=None): from homely._engine2 import initengine, resetengine, setrepoinfo assert cancleanup is not None if only is None: only = [] elif len(only): assert len(infos) <= 1 global _CURRENT_REPO errors = False if not _writepidfile(): return False isfullupdate = False if (cancleanup and (not len(only)) and len(infos) == RepoListConfig().repo_count()): isfullupdate = True # remove the fail file if it is still hanging around if os.path.exists(FAILFILE): os.unlink(FAILFILE) try: # write the section file with the current section name _write(SECTIONFILE, "<preparing>") engine = initengine() for info in infos: setrepoinfo(info) assert isinstance(info, RepoInfo) _CURRENT_REPO = info localrepo = info.localrepo with entersection(os.path.basename(localrepo.repo_path)), \ head("Updating from {} [{}]".format( localrepo.repo_path, info.shortid())): if pullfirst: with note("Pulling changes for {}".format( localrepo.repo_path)): if localrepo.isdirty(): dirty("Aborting - uncommitted changes") else: try: localrepo.pullchanges() except ConnectionError: noconn("Could not connect to remote server") # make sure the HOMELY.py script exists pyscript = os.path.join(localrepo.repo_path, 'HOMELY.py') if not os.path.exists(pyscript): warn("{}: {}".format(ERR_NO_SCRIPT, localrepo.repo_path)) continue if len(only): engine.onlysections(only) try: homely._utils._loadmodule('HOMELY', pyscript) except Exception as err: import traceback tb = traceback.format_exc() warn(str(err)) for line in tb.split('\n'): warn(line) # Remove 'HOMELY' from sys modules so it is ready for the next # run. Note that if the call to load_module() failed then the # HOMELY module might not be present. sys.modules.pop('HOMELY', None) setrepoinfo(None) if isfullupdate: if _NOTECOUNT.get('warn'): note("Automatic Cleanup not possible due to previous warnings") else: _write(SECTIONFILE, "<cleaning up>") engine.cleanup(engine.WARN) resetengine() os.unlink(SECTIONFILE) except KeyboardInterrupt: errors = True raise except Exception as err: warn(str(err)) import traceback tb = traceback.format_exc() for line in tb.split('\n'): warn(line) errors = True finally: warncount = _NOTECOUNT.get('warn') noconncount = _NOTECOUNT.get('noconn') dirtycount = _NOTECOUNT.get('dirty') if isfullupdate: if errors or warncount: # touch the FAILFILE if there were errors or warnings with open(FAILFILE, 'w') as f: pass elif noconncount: with open(FAILFILE, 'w') as f: f.write(UpdateStatus.NOCONN) elif dirtycount: with open(FAILFILE, 'w') as f: f.write(UpdateStatus.DIRTY) _write(TIMEFILE, time.strftime("%H:%M")) if os.path.exists(RUNFILE): os.unlink(RUNFILE) return not (errors or warncount or noconncount or dirtycount)
def autoupdate(**kwargs): options = ('pause', 'unpause', 'outfile', 'daemon', 'clear') action = None for name in options: if kwargs[name]: if action is not None: raise UsageError("--%s and --%s options cannot be combined" % (action, name)) action = name if action is None: raise UsageError("Either %s must be used" % (" or ".join("--{}".format(o) for o in options))) mkcfgdir() if action == "pause": with open(PAUSEFILE, 'w'): pass return if action == "unpause": if os.path.exists(PAUSEFILE): os.unlink(PAUSEFILE) return if action == "clear": if os.path.exists(FAILFILE): os.unlink(FAILFILE) return if action == "outfile": print(OUTFILE) return # is an update necessary? assert action == "daemon" # check if we're allowed to start an update status, mtime, _ = getstatus() if status == UpdateStatus.FAILED: print("Can't start daemon - previous update failed") sys.exit(1) if status == UpdateStatus.PAUSED: print("Can't start daemon - updates are paused") sys.exit(1) if status == UpdateStatus.RUNNING: print("Can't start daemon - an update is already running") sys.exit(1) # abort the update if it hasn't been long enough interval = 20 * 60 * 60 if mtime is not None and (time.time() - mtime) < interval: print("Can't start daemon - too soon to start another update") sys.exit(1) assert status in (UpdateStatus.OK, UpdateStatus.NEVER, UpdateStatus.NOCONN) oldcwd = os.getcwd() import daemon with daemon.DaemonContext(), open(OUTFILE, 'w') as f: try: from homely._ui import setstreams setstreams(f, f) # we need to chdir back to the old working directory or imports # will be broken! if sys.version_info[0] < 3: os.chdir(oldcwd) cfg = RepoListConfig() run_update(list(cfg.find_all()), pullfirst=True, cancleanup=True) except Exception: import traceback f.write(traceback.format_exc()) raise
def addfromremote(repo, dest_path): assert isinstance(repo, Repo) and repo.isremote rlist = RepoListConfig() if repo.iscanonical: # abort if we have already added this repo before match = rlist.find_by_canonical(repo.repo_path) if match: note("Repo [%s] from %s has already been added" % (match.shortid(), repo.repo_path)) return match, True # figure out where the temporary clone should be moved to after it is # created if dest_path is None: assert repo.suggestedlocal is not None dest_path = os.path.join(os.environ["HOME"], repo.suggestedlocal) with tmpdir(os.path.basename(dest_path)) as tmp: # clone the repo to a temporary location note("HOME: %s" % os.environ["HOME"]) note("tmp: %s" % tmp) note("Cloning %s to tmp:%s" % (repo.repo_path, tmp)) repo.clonetopath(tmp) # find out the first commit id localrepo = repo.frompath(tmp) assert isinstance(localrepo, repo.__class__) tmprepoid = localrepo.getrepoid() # if we recognise the repo, record the canonical path onto # the repo info so we don't have to download it again match = rlist.find_by_id(tmprepoid) if match is not None: note("Repo [%s] from has already been added" % match.localrepo.shortid(tmprepoid)) if repo.iscanonical: match.canonicalrepo = repo rlist.add_repo(match) rlist.writejson() return match, True if os.path.exists(dest_path): destrepo = localrepo.frompath(dest_path) if not destrepo: # TODO: use a different type of exception here raise Exception("DEST_PATH %s already exists" % dest_path) # check that the repo there is the right repo destid = destrepo.getrepoid() if destid != tmprepoid: # TODO: this should be a different type of exception raise Exception("Repo with id [%s] already exists at %s" % (destrepo.getrepoid(False), dest_path)) # we can use the repo that already exists at dest_path note("Using the existing repo [%s] at %s" % (destrepo.shortid(destid), dest_path)) return RepoInfo(destrepo, destid), True # move our temporary clone into the final destination os.rename(tmp, dest_path) destrepo = localrepo.frompath(dest_path) assert destrepo is not None info = RepoInfo(destrepo, destrepo.getrepoid(), repo if repo.iscanonical else None, ) return info, False