def _loadDates(self): revision = 'HEAD' if self.getRef() == None else self.getRef() cmd = ('git log' + ' --topo-order' + ' --reverse' + ' --pretty=format:"%H %at %an %ae"' + ' ' + revision + ' -- ' + config.ISSUES_DIR + "/" + self.getId()) commits = getCmd(cmd) if commits: commits = commits.split('\n') self._createdDate = commits[0].split()[1] self._createdAuthorName = commits[0].split()[2] self._createdAuthorEmail = commits[0].split()[3] self._modifiedDate = commits[-1].split()[1] self._modifiedAuthorName = commits[-1].split()[2] self._modifiedAuthorEmail = commits[-1].split()[3] else: self._createdDate = 0 self._createdAuthorName = getCmd("git config user.name") self._createdAuthorEmail = getCmd("git config user.email") self._modifiedDate = 0 self._modifiedAuthorName = getCmd("git config user.name") self._modifiedAuthorEmail = getCmd("git config user.email")
def prependGhiCommitMsg(commitMsgFilepath): """Examines the git index that is about to be committed and adds auto-generated commit message lines into the git commit message to represent any ghi related changes.""" relIssuesPath = config.ISSUES_DIR[len(config.GIT_ROOT) + 1:] # +1 to remove '/' relGroupsPath = config.GROUPS_DIR[len(config.GIT_ROOT) + 1:] # +1 to remove '/' # Find any .ghi files in the index ghiMods = getCmd('git diff --cached --name-status -- ' + config.GHI_DIR) if ghiMods == None: # Nothing of interest in index... bail return None # Build a set of suggested commit message lines based on the # changes to .ghi files in the index commitMsg=['\n'] for mod in ghiMods.split('\n'): m = mod.split() # if file was an issue if m[1].count(relIssuesPath): issueID = m[1][len(relIssuesPath)+1:] # if issue was added if m[0] == 'A': commitMsg.extend(["# " + buildGhiAddMsg(issueID) + "\n"]) # if issue was modified elif m[0] == 'M': commitMsg.extend(["# " + buildGhiEditMsg(issueID) + "\n"]) # if issue was deleted elif m[0] == 'D': commitMsg.extend(["# " + buildGhiRmMsg(issueID) + "\n"]) elif m[1].count(relGroupsPath): groupname = m[1][len(relGroupsPath)+1:] # Run through the group file and see exactly what was done. diff = getCmd("git diff --cached -- " + m[1]).split('\n') for line in diff: # Issue added if line[0] == '+' and not line[0:3] == '+++': commitMsg.extend(["# " + buildGhiGroupAddMsg(groupname, line[1:]) + "\n"]) # Issue removed if line[0] == '-' and not line[0:3] == '---': commitMsg.extend(["# " + buildGhiGroupRmMsg(groupname, line[1:]) + "\n"]) # Add the existing commit message (usually a default git template) with open(commitMsgFilepath, 'rb') as f: commitMsg.extend(f.readlines()) # Now write out the full message with open(commitMsgFilepath, 'wb') as f: for line in commitMsg: f.write(line)
def execute(args): # First validate arguments issueID = identifiers.getFullIssueIdFromLeadingSubstr(args.id) if issueID == None: # ID is required... no good print "Could not find issue: " + args.id return None # Load the existing issue issue = IssueFile.readIssueFromDisk( config.ISSUES_DIR + "/" + issueID); # Are we going to use interactive editing? if args.status == None and args.title == None and args.description == None: tmpFile = config.GHI_DIR + "/" + "ISSUE_EDIT"; IssueFile.writeEditableIssueToDisk(tmpFile, issue) tmpFileHash = getCmd("git hash-object " + tmpFile) subprocess.call([config.GIT_EDITOR, tmpFile]) issue = IssueFile.readEditableIssueFromDisk(tmpFile) # Check to see if the tmpFile is unchanged if (tmpFileHash == getCmd("git hash-object " + tmpFile)): print "No change in Issue data. Issue not updated" return None # Set the status if args.status: # There is a potential bug here in situations where there is # more than one status with the same value name statusUpdate = None for k,v in config.STATUS_OPTS.iteritems(): if args.status == v: statusUpdate = k break if statusUpdate != None: issue.setStatus(statusUpdate) else: print "Status does not exist!" return None # Set title if args.title: issue.setTitle(args.title) # Set description if args.description: issue.setDescription(args.description) # Make changes to index for commit issuepath = config.ISSUES_DIR + "/" + issueID IssueFile.writeIssueToDisk(issuepath, issue) commit_helper.addToIndex(issuepath) if args.commit: commit_helper.commit()
def cleanWcAndCommitGhiDir(msg): '''Cleans the git working copy and creates a git commit for the whole .ghi directory''' _stashWc() getCmd("git add " + config.GHI_DIR) cmdName = _getCallerModuleName() print getCmd('git commit -m "ghi-' + cmdName + ' ' + msg + '"') _restoreWc()
def execute(args): issue = None # First validate arguments if (args.title == None and args.description == None): # If no arguments, drop into interactive mode tmpFile = config.GHI_DIR + "/" + "ISSUE_EDIT"; issue = IssueProto() IssueFile.writeEditableIssueToDisk(tmpFile, issue) tmpFileHash = getCmd("git hash-object " + tmpFile) subprocess.call([config.GIT_EDITOR, tmpFile]) issue = IssueFile.readEditableIssueFromDisk(tmpFile) # Check to see if the tmpFile is unchanged if (tmpFileHash == getCmd("git hash-object " + tmpFile)): print "Not enough data to create issue. No issue created." return None elif (args.title == None): # Title is required... no good print "An issue title is required. Try 'add' with no arguments for interactive mode" return None else: # Create new issue issue = IssueProto(); # Set title issue.setTitle(args.title) # Set description if (args.description): issue.setDescription(args.description) if (issue): # Generate an issue ID issueID = str(identifiers.genNewIssueID()) # Make changes to index for commit issuepath = config.ISSUES_DIR + "/" + issueID IssueFile.writeIssueToDisk(issuepath, issue) commit_helper.addToIndex(issuepath) if args.group: Group(args.group).addIssue(issueID) commit_helper.addToIndex(config.GROUPS_DIR + '/"' + args.group + '"') # Display the new issue ID to the user print issueID if args.commit: commit_helper.commit()
def _loadIssueFile(self): if self._ref == None: tmp = IssueFile.readIssueFromDisk( config.ISSUES_DIR + '/' + self._id) else: getCmd('git show ' + self.getRef() + ':' + config.mkPathRel(config.ISSUES_DIR) + '/' + self._id + ' > ' + config.mkPathRel(config.GHI_DIR) + '/tmpfile') tmp = IssueFile.readIssueFromDisk(config.GHI_DIR + '/tmpfile') getCmd('rm ' + config.GHI_DIR + '/tmpfile') self.setTitle(tmp.getTitle()) self.setStatus(tmp.getStatus()) self.setDescription(tmp.getDescription())
def getGroupsForIssueId(issueID): groupPaths = getCmd("git grep --name-only " + issueID + " -- " + config.GROUPS_DIR) groups = [] pathPrefix = config.GROUPS_DIR[len(config.GIT_ROOT) + 1:] # +1 to remove '/' if groupPaths != None: for path in groupPaths.splitlines(): groupname = path[len(pathPrefix) + 1:] # +1 to remove '/' groups.extend([groupname]) return groups
def execute(args): issueIDs = _getFilteredListofIssueIDs(args) if issueIDs == None: print "Could not find issue: " + args.id return elif len(issueIDs) == 1: print IssueDisplayBuilder(issueIDs[0]).getFullIssueDisplay() else: columns = [display.COLUMNS['id'], display.COLUMNS['status'], display.COLUMNS['groups'], display.COLUMNS['title'], display.COLUMNS['mdate']] # We may have a lot of issues in the list that would make the output # run pretty long, therefore page it. PageOutputBeyondThisPoint() header = ' '.join([truncateOrPadStrToWidth(col.name, col.length) for col in columns]) print header print '-' * len(header) # Any sorting required? if args.sort != None: issueIDs = _sortIssues(issueIDs, args.sort) # Check to see if a default issue sort has been configured for this repository else: sort = getCmd('git config issue.ls.sort') if sort != None: issueIDs = _sortIssues(issueIDs, sort) # Group arg can be passed as parameter or via configured default if args.group or getCmd('git config issue.ls.group') == 'true': _displayGrouped(issueIDs, columns) else: _displayUnGrouped(issueIDs, columns)
def _SelectPager(): try: return os.environ['GIT_PAGER'] except KeyError: pass pager = getCmd('git config core.pager') if pager: return pager try: return os.environ['PAGER'] except KeyError: pass return 'less'
def execute(args): if (args.id): issueID = getFullIssueIdFromLeadingSubstr(args.id) if issueID == None: print "Could not find issue: " + args.id return None # See if we can remove this issue at all without a --force issuePath = getPathFromId(issueID) if not args.force: issueStatus = getCmd("git status --porcelain -- " + issuePath) if issueStatus and issueStatus[0] =='A': print "Cannot remove issue without --force" return None # Remove the issue from any groups that contained it groupnames = group.getGroupsForIssueId(issueID) # If we're not forcing the remove, then we need to double-check # to make sure that we can actually remove the issue from each # group without breaking things... this seems like hack... # Why should we be having to check first before we execute later? # Should we just perform the change on the group objects and then # commit them?... maybe I'm missing something and this isn't a big deal. for name in groupnames: if not Group(name)._canRmIssueFromGroup(issueID,args.force): # Can't perform this operation without a force! print "Cannot remove issue from group '" + group + "' without --force" return None # All clear to remove the issue!... groups first if you please... for name in groupnames: Group(name).rmIssue(issueID, args.force) # HACK HACK HACK # Should be executing a git command here to add the # subsequent group changes to the index, but I'm taking # a shortcut for the moment issueTitle = Issue(issueID).getTitle() # Remove the issue commit_helper.remove(issuePath, args.force) if args.commit: commit_helper.commit()
def STATUS_OPTS(self): '''List of legal issue status values''' from subprocess_helper import getCmd if not hasattr(self, '_STATUS_OPTS'): # Set the default status options to be used in case no # config file is present (shouldn't happen, but whatevs) self._STATUS_OPTS = {0:'New',1:'In progress',2:'Fixed'} status_options = getCmd('git config ' + '-f ' + self.GHI_DIR + '/config ' + '--get-regexp status.s') if status_options != None: for status in status_options.split('\n'): key,sep,val = str(status).partition(' ') self._STATUS_OPTS[int(key.lstrip('status.s'))] = val return self._STATUS_OPTS
def _sortIssues(issueIDs, sortBy): if sortBy == None: return None if sortBy == 'id': # We don't need to do anything here. Since the issues are stored with the id as the # filename, then they will be automatically sorted. return issueIDs issuesPathPrefix = config.ISSUES_DIR[len(config.GIT_ROOT) + 1:] # +1 to remove '/' issueSortTuple =[] if sortBy == 'title': for issueID in issueIDs: issueSortTuple.extend([[Issue(issueID).getTitle(), getFullIssueIdFromLeadingSubstr(issueID)]]) elif sortBy == 'status': # Organize the issues into status groups for sk in config.STATUS_OPTS: issues = getCmd('git grep -n ^' + str(sk) + '$ -- ' + config.ISSUES_DIR) if issues != None: for i in issues.splitlines(): issueSortTuple.extend([[sk, i.split(':')[0][len(issuesPathPrefix) + 1:]]]) # +1 to remove '/' elif sortBy == 'date' or sortBy == 'cdate': for issueID in issueIDs: issueSortTuple.extend([[Issue(issueID).getCreatedDate(), getFullIssueIdFromLeadingSubstr(issueID)]]) elif sortBy == 'mdate': for issueID in issueIDs: issueSortTuple.extend([[Issue(issueID).getModifiedDate(), getFullIssueIdFromLeadingSubstr(issueID)]]) # Sort by Date issueSortTuple.sort(key=lambda issue: issue[0]) if len(issueSortTuple) > 0: return map (lambda issueID: issueID[1], issueSortTuple) return None
def _canRmIssueFromGroup(self, issueID, force = False): if force: return True groupIDs = self.getIssueIds() if groupIDs.count(issueID) == 0 or len(groupIDs) == 0: return False # If there is more than one issue in the group we can always # successfully remove (assuming the issue is in this group) if len(groupIDs) > 1: return True # If this group only has one issue and we're trying to remove # it then we have to remove the group file as well. If this is # the case and the group is already modified in the git # index then we need a force to make this happen else: # len(groupIDs) == 1 groupGitStatus = getCmd('git status --porcelain -- "' + self.getPath() + '"') if groupGitStatus and groupGitStatus[0] != " ": return False return True
def rmIssue(self, issueID, force = False): if not self._canRmIssueFromGroup(issueID, force): return False groupIDs = self.getIssueIds() # If we're removing the last issue in a file, then rm the file if len(groupIDs) == 1 and groupIDs[0] == issueID: # HACK HACK HACK # Should not be executing a git command here if force: getCmd('git rm -f "' + self.getPath() + '"') else: getCmd('git rm "' + self.getPath() + '"') else: with open(self.getPath(), "wb") as f: for identifier in groupIDs: if not identifier == issueID: f.write(identifier + "\n") # HACK HACK HACK # Should not be executing a git command here getCmd('git add "' + self.getPath() + '"')
def execute(args): bFileAdd = False # Check to see if the .ghi directories have already been created # If it doesn't exist, create it. if os.path.isdir(config.GHI_DIR) == False: os.makedirs(config.GHI_DIR) _writeGhiDirGitIgnoreFile(config.GHI_DIR + "/.gitignore") bFileAdd = True if os.path.isdir(config.ISSUES_DIR) == False: os.makedirs(config.ISSUES_DIR) # Touch a .gitignore file in the ISSUES_DIR so that we can # track the directory in git before any issues get added getCmd("touch " + config.ISSUES_DIR + "/.gitignore") bFileAdd = True if os.path.isdir(config.GROUPS_DIR) == False: os.makedirs(config.GROUPS_DIR) # Touch a .gitignore file in the GROUPS_DIR so that we can # track the directory in git before any issues get added getCmd("touch " + config.GROUPS_DIR + "/.gitignore") bFileAdd = True for key, val in config.STATUS_OPTS.iteritems(): getCmd('git config ' + '-f ' + config.GHI_DIR + '/config ' + 'status.s' + str(key) + ' "' + val + '"') if bFileAdd: commit_helper.cleanWcAndCommitGhiDir("Initializing ghi") # Alias "git issue" to ghi if args.ghi_path: getCmd("git config alias.issue '!" + args.ghi_path + "/ghi'") ghicmdpath = args.ghi_path else: getCmd("git config alias.issue '!ghi'") ghicmdpath = "ghi" # Insert git hooks getCmd("cp " + ghicmdpath + "/hooks/prepare-commit-msg " + config.GIT_ROOT + "/.git/hooks/") # Clever successful response message... in the future it would # be nice if running 'ghi-init' on a git that already has issues # would give you a summary of stats on the current issues of that # git print "Initialized ghi, but this git currently has no issues"
def execute(args): # Are we deleting something? if args.d or args.D: # see if we're deleting an existing issue from a group issueToDelete = args.d if args.d else args.D issueID = identifiers.getFullIssueIdFromLeadingSubstr(issueToDelete) if issueID: force = False if args.d else True # If no groupname is given, then we will remove from all groups # ... notice the hack here where args.id is holding the groupname # due to the currently lame and hacky argparsing if not args.id: # Remove the issue from any groups that contained it groupnames = group.getGroupsForIssueId(issueID) if len(groupnames) == 0: print "No groups to delete issue from!" return None # If we're not forcing the remove, then we need to double-check # to make sure that we can actually remove the issue from each # group without breaking things for name in groupnames: if not Group(name)._canRmIssue(issueID,force): # Can't perform this operation without a force! print "Cannot delete issue from group '" + name + "' without force option, '-D'" return None # All clear to remove the issue!... groups first if you please... for name in groupnames: Group(name).rmIssue(issueID,force) # HACK HACK HACK # Should be executing a git command here to add the # subsequent group changes to the index, but I'm taking # a shortcut for the moment return None # HACK HACK HACK # The command line parsing here is totally messed up and so # rather than using the groupname we have to pretend here # that the id is the groupname... the command line just # needs to be rewritten :( Group(args.id).rmIssue(issueID, force) # HACK HACK HACK # Should be executing a git command here to add the # subsequent group changes to the index, but I'm taking # a shortcut for the moment return None # see if we're deleting a group entirely if group.exists(args.d): print "groupname = " + args.d getCmd('git rm "' + Group(args.d).getPath() + '"') return None elif group.exists(args.D): print "groupname = " + args.D getCmd('git rm -f "' + Group(args.D).getPath() + '"') return None # tried to delete, but we couldn't figure out what... groupname = args.d if args.d else args.D print "Could not delete '" + groupname + "' without force option, '-D'" return None if args.groupname == None and args.id == None: print "\n".join(group.getListOfAllGroups()) return None if args.groupname == None: # We don't support this syntax yet print "Command not currently supported" return None # get the full issue ID & Add the issue to the group issueID = identifiers.getFullIssueIdFromLeadingSubstr(args.id) Group(args.groupname).addIssue(issueID) commit_helper.addToIndex('"' + Group(args.groupname).getPath() + '"') if args.commit: commit_helper.commit()
def GIT_EDITOR(self): '''Get the root directory for all ghi files''' from subprocess_helper import getCmd if not hasattr(self, '_GIT_EDITOR'): self._GIT_EDITOR = getCmd('git config core.editor') return self._GIT_EDITOR
def remove(path, force=False): if force: getCmd("git rm -f " + path) else: getCmd("git rm " + path)
def commit(msg = None): if msg: getCmd('git commit -m "' + msg + '"') else: runCmd('git commit')
def _restoreWc(): # Now restore the working copy getCmd("git stash pop")
def _stashWc(): # Adding an issue includes adding a new commit to the repository. # To make sure that our commit doesn't include anything other # than this issue, clean the working copy using git-stash getCmd("git stash --all")
def addToIndex(path): getCmd("git add " + path)
def GIT_ROOT(self): '''Get the git top-level directory''' from subprocess_helper import getCmd if not hasattr(self, '_GIT_ROOT'): self._GIT_ROOT = getCmd('git rev-parse --show-toplevel') return self._GIT_ROOT