Recently, the following code was written to help automate some processes that are typically executed while working with a Mercurial database. What is shown below is basically a first draft of the program and was quickly written to test out the idea of creating a GUI tool for others to use.
Question: Are there any obvious flaws in how the program works or how the code is formatted and arranged that might be improved for future versions?
import sys
import subprocess
import tkFileDialog
import Tkinter
class TortoiseHelper(Tkinter.Tk):
def __init__(self):
Tkinter.Tk.__init__(self)
self.initialize()
self.create_controls()
if len(sys.argv) > 1:
if TortoiseHelper.validate_repo(sys.argv[1]):
self.repoPath.set(sys.argv[1])
self.refresh_repo()
def change_repo(self):
newPath = tkFileDialog.askdirectory(parent=self)
if TortoiseHelper.validate_repo(newPath):
self.repoPath.set(newPath)
self.refresh_repo()
def create_controls(self):
mainFrame = Tkinter.Frame(self)
mainFrame.grid(row=0, column=0, padx=10, pady=10, sticky=(Tkinter.N, Tkinter.E, Tkinter.S, Tkinter.W))
mainFrame.columnconfigure(1, weight=1)
mainFrame.rowconfigure(13, weight=1)
repoLabel = Tkinter.Label(mainFrame)
repoLabel['text'] = 'Repository'
repoLabel.grid(row=0, column=0, sticky=Tkinter.E)
lblRepo = Tkinter.Label(mainFrame)
lblRepo['textvariable'] = self.repoPath
lblRepo['relief'] = Tkinter.SUNKEN
lblRepo['anchor'] = Tkinter.W
lblRepo.grid(row=0, column=1, ipadx=4, pady=2, sticky=(Tkinter.E, Tkinter.W))
btnRefresh = Tkinter.Button(mainFrame)
btnRefresh['text'] = 'Refresh'
btnRefresh['command'] = self.refresh_repo
btnRefresh.grid(row=0, column=2, padx=2)
btnChange = Tkinter.Button(mainFrame)
btnChange['text'] = 'Change'
btnChange['command'] = self.change_repo
btnChange.grid(row=0, column=3, padx=2)
statusLabel = Tkinter.Label(mainFrame)
statusLabel['text'] = 'Status'
statusLabel.grid(row=1, column=0, sticky=(Tkinter.N, Tkinter.E))
lblStatus = Tkinter.Label(mainFrame)
lblStatus['textvariable'] = self.repoStatus
lblStatus['relief'] = Tkinter.SUNKEN
lblStatus['anchor'] = Tkinter.W
lblStatus['justify'] = Tkinter.LEFT
lblStatus.grid(row=1, column=1, ipadx=4, pady=2, sticky=(Tkinter.E, Tkinter.W))
branchLabel = Tkinter.Label(mainFrame)
branchLabel['text'] = 'Branch'
branchLabel.grid(row=2, column=0, sticky=Tkinter.E)
lblBranch = Tkinter.Label(mainFrame)
lblBranch['textvariable'] = self.repoBranch
lblBranch['relief'] = Tkinter.SUNKEN
lblBranch['anchor'] = Tkinter.W
lblBranch.grid(row=2, column=1, ipadx=4, pady=2, sticky=(Tkinter.E, Tkinter.W))
chkCommit = Tkinter.Checkbutton(mainFrame)
chkCommit['text'] = 'Commit'
chkCommit['variable'] = self.optionCommit
chkCommit['command'] = self.on_commit_click
chkCommit.grid(row=3, column=0, columnspan=4, sticky=Tkinter.W)
branchNameLabel = Tkinter.Label(mainFrame)
branchNameLabel['text'] = 'New branch name'
branchNameLabel.grid(row=4, column=0, sticky=Tkinter.E)
self.entBranchName = Tkinter.Entry(mainFrame)
self.entBranchName['textvariable'] = self.newBranchName
self.entBranchName.grid(row=4, column=1, columnspan=3, sticky=(Tkinter.E, Tkinter.W))
commitMessageLabel = Tkinter.Label(mainFrame)
commitMessageLabel['text'] = 'Commit message'
commitMessageLabel.grid(row=5, column=0, sticky=Tkinter.E)
self.txtCommitMessage = Tkinter.Text(mainFrame)
self.txtCommitMessage['height'] = 4
self.txtCommitMessage.grid(row=5, column=1, columnspan=3, pady=2, sticky=(Tkinter.E, Tkinter.W))
self.chkClose = Tkinter.Checkbutton(mainFrame)
self.chkClose['text'] = 'Close branch'
self.chkClose['variable'] = self.optionClose
self.chkClose.grid(row=6, column=0, columnspan=4, sticky=Tkinter.W)
self.chkMergeTest = Tkinter.Checkbutton(mainFrame)
self.chkMergeTest['text'] = 'Merge with Test'
self.chkMergeTest['variable'] = self.optionMergeTest
self.chkMergeTest['command'] = self.on_merge_test_click
self.chkMergeTest.grid(row=7, column=0, columnspan=4, sticky=Tkinter.W)
self.chkMergeTestSilent = Tkinter.Checkbutton(mainFrame)
self.chkMergeTestSilent['text'] = 'Attempt silent merge'
self.chkMergeTestSilent['variable'] = self.optionSilentMergeTest
self.chkMergeTestSilent.grid(row=8, column=0, columnspan=4, padx=20, sticky=Tkinter.W)
self.chkMergeProduction = Tkinter.Checkbutton(mainFrame)
self.chkMergeProduction['text'] = 'Merge with Production'
self.chkMergeProduction['variable'] = self.optionMergeProduction
self.chkMergeProduction['command'] = self.on_merge_production_click
self.chkMergeProduction.grid(row=9, column=0, columnspan=4, sticky=Tkinter.W)
self.chkMergeProductionSilent = Tkinter.Checkbutton(mainFrame)
self.chkMergeProductionSilent['text'] = 'Attempt silent merge'
self.chkMergeProductionSilent['variable'] = self.optionSilentMergeProduction
self.chkMergeProductionSilent.grid(row=10, column=0, columnspan=4, padx=20, sticky=Tkinter.W)
chkPush = Tkinter.Checkbutton(mainFrame)
chkPush['text'] = 'Push'
chkPush['variable'] = self.optionPush
chkPush.grid(row=11, column=0, columnspan=4, sticky=Tkinter.W)
btnRun = Tkinter.Button(mainFrame)
btnRun['text'] = 'Run'
btnRun['command'] = self.run
btnRun.grid(row=12, column=0, sticky=Tkinter.W)
btnCancel = Tkinter.Button(mainFrame)
btnCancel['text'] = 'Cancel'
btnCancel['command'] = self.quit
btnCancel.grid(row=12, column=2, columnspan=3, sticky=Tkinter.E)
self.txtOutput = Tkinter.Text(mainFrame)
self.txtOutput['state'] = Tkinter.DISABLED
self.txtOutput.grid(row=13, column=0, pady=4, columnspan=4, sticky=(Tkinter.N, Tkinter.E, Tkinter.S, Tkinter.W))
self.txtOutput.tag_config('RED', foreground='red')
self.txtOutput.tag_config('GRAY', foreground='gray')
self.txtOutput.tag_config('BG_GRAY', background='gray')
def initialize(self):
# Initialize variables
self.repoPath = Tkinter.StringVar()
self.repoStatus = Tkinter.StringVar()
self.repoBranch = Tkinter.StringVar()
self.optionCommit = Tkinter.IntVar()
self.newBranchName = Tkinter.StringVar()
self.optionClose = Tkinter.IntVar()
self.optionMergeTest = Tkinter.IntVar()
self.optionSilentMergeTest = Tkinter.IntVar()
self.optionMergeProduction = Tkinter.IntVar()
self.optionSilentMergeProduction = Tkinter.IntVar()
self.optionPush = Tkinter.IntVar()
# Configure the window
self.title('Tortoise Helper')
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.minsize(width=400, height=500)
@staticmethod
def is_special_branch(branchName):
return branchName in ('Test', 'Production', 'Site Redesign')
def on_commit_click(self):
self.set_commit_fields_enabled()
self.set_close_branch_option_enabled()
self.set_merge_test_option_enabled()
self.set_merge_production_option_enabled()
def on_merge_production_click(self):
self.set_silent_merge_production_option_enabled()
def on_merge_test_click(self):
self.set_silent_merge_test_option_enabled()
def refresh_repo(self):
path = self.repoPath.get()
command = 'hg status -R ' + path
uncommittedChanges = TortoiseHelper.run_command(command)['full_output'] != ''
repositoryStatus = ''
if uncommittedChanges == 0:
repositoryStatus = 'No uncommitted changes'
elif uncommittedChanges == 1:
repositoryStatus = '1 uncommitted change'
else:
repositoryStatus = str(uncommittedChanges) + ' uncommitted changes'
command = 'hg incoming -q -R ' + path
incomingChanges = len(TortoiseHelper.run_command(command)['output'])
if incomingChanges == 0:
repositoryStatus += '\nNo incoming changes'
elif incomingChanges == 1:
repositoryStatus += '\n1 incoming change'
else:
repositoryStatus += '\n' + str(incomingChanges) + ' incoming changes'
command = 'hg outgoing -q -R ' + path
outgoingChanges = len(TortoiseHelper.run_command(command)['output'])
if outgoingChanges == 0:
repositoryStatus += '\nNo outgoing changes'
elif outgoingChanges == 1:
repositoryStatus += '\n1 outgoing change'
else:
repositoryStatus += '\n' + str(outgoingChanges) + ' outgoing changes'
self.repoStatus.set(repositoryStatus)
command = 'hg branch -R ' + path
branchName = TortoiseHelper.run_command(command)['full_output']
self.repoBranch.set(branchName)
self.optionCommit.set(0)
self.set_commit_fields_enabled()
self.set_close_branch_option_enabled()
self.set_merge_production_option_enabled()
self.set_merge_test_option_enabled()
self.optionPush.set(0)
self.txtOutput.delete(1.0, Tkinter.END)
def run(self):
# Determine what needs to be done
command = ''
currentRevision = ''
useMergeDialog = False
path = self.repoPath.get()
command = 'hg branch -R ' + path
currentBranchName = TortoiseHelper.run_command(command)['full_output']
commit = self.optionCommit.get() == 1
newBranchName = self.newBranchName.get()
changeBranch = newBranchName != currentBranchName
commitMessage = self.txtCommitMessage.get(1.0, Tkinter.END).strip()
closeBranch = self.optionClose.get() == 1
mergeWithTest = self.optionMergeTest.get() == 1
silentMergeTest = self.optionSilentMergeTest.get() == 1
mergeWithProduction = self.optionMergeProduction.get() == 1
silentMergeProduction = self.optionSilentMergeProduction.get() == 1
push = self.optionPush.get() == 1
# Validate
if commit:
if newBranchName == '':
self.write_output('Branch name is required.', 'RED')
return
if TortoiseHelper.is_special_branch(newBranchName):
self.write_output('Branch name is not valid.', 'RED')
return
if commitMessage == '':
self.write_output('Commit message is required', 'RED')
return
if closeBranch:
if not commit:
if TortoiseHelper.is_special_branch(currentBranchName):
self.write_output('Cannot close branch ' + currentBranchName + '.', 'RED')
return
# Execute
if commit:
if changeBranch:
self.write_output('Changing branch name', 'BG_GRAY')
command = 'hg branch -R ' + path + ' "' + newBranchName + '"'
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
currentBranchName = newBranchName
self.write_output('Committing', 'BG_GRAY')
command = 'hg commit -R ' + path + ' -v "--message=' + commitMessage + '"'
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
if closeBranch:
self.write_output('Closing', 'BG_GRAY')
command = 'hg commit -R ' + path + ' -v --close-branch "--message=Close ' + currentBranchName + ' branch"'
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
if mergeWithTest or mergeWithProduction:
command = 'hg identify -i -R ' + path
self.write_output(command)
currentRevision = TortoiseHelper.run_command(command)['full_output']
if mergeWithTest:
self.write_output('Pulling', 'BG_GRAY')
command = 'hg pull -R ' + path
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
self.write_output('Updating to Test', 'BG_GRAY')
command = 'hg update -R ' + path + ' Test'
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
if silentMergeTest:
useMergeDialog = False
self.write_output('Attempting silent merge', 'BG_GRAY')
command = 'hg merge -R ' + path + ' -t internal:merge -r ' + currentRevision
self.write_output(command)
command_result = TortoiseHelper.run_command(command)
self.write_output(command_result['full_output'], 'GRAY')
if command_result['return_code'] == 1:
self.write_output('Silent merge failed; returning to Test branch', 'BG_GRAY')
command = 'hg update -C -R ' + path
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
useMergeDialog = True
else:
useMergeDialog = True
if useMergeDialog:
self.write_output('Launching merge dialog', 'BG_GRAY')
command = 'thg merge -R ' + path + ' -r ' + currentRevision
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
if mergeWithProduction:
self.write_output('Pulling', 'BG_GRAY')
command = 'hg pull -R ' + path
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
self.write_output('Updating to Production', 'BG_GRAY')
command = 'hg update -R ' + path + ' Production'
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
if silentMergeProduction:
useMergeDialog = False
self.write_output('Attempting silent merge', 'BG_GRAY')
command = 'hg merge -R ' + path + ' -t internal:merge -r ' + currentRevision
self.write_output(command)
command_result = TortoiseHelper.run_command(command)
self.write_output(command_result['full_output'], 'GRAY')
if command_result['return_code'] == 1:
self.write_output('Silent merge failed; returning to Production branch', 'BG_GRAY')
command = 'hg update -C -R ' + path
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
useMergeDialog = True
else:
useMergeDialog = True
if useMergeDialog:
self.write_output('Launching merge dialog', 'BG_GRAY')
command = 'thg merge -R ' + path + ' -r ' + currentRevision
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
if push:
self.write_output('Pushing', 'BG_GRAY')
command = 'hg push -R ' + path + ' --new-branch'
self.write_output(command)
self.write_output(TortoiseHelper.run_command(command)['full_output'], 'GRAY')
@staticmethod
def run_command(command):
output = []
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for line in process.stdout:
output.append(line.strip())
return { "return_code": process.wait(), "output": output, "full_output": '\n'.join(output) }
def set_close_branch_option_enabled(self):
if TortoiseHelper.is_special_branch(self.repoBranch.get()) and self.optionCommit.get() == 0:
self.chkClose['state'] = Tkinter.DISABLED
self.optionClose.set(0)
else:
self.chkClose['state'] = Tkinter.NORMAL
def set_commit_fields_enabled(self):
if self.optionCommit.get() == 1:
self.entBranchName['state'] = Tkinter.NORMAL
self.txtCommitMessage['state'] = Tkinter.NORMAL
else:
self.entBranchName['state'] = Tkinter.DISABLED
self.newBranchName.set(self.repoBranch.get())
self.txtCommitMessage['state'] = Tkinter.DISABLED
self.txtCommitMessage.delete(1.0, Tkinter.END)
def set_merge_production_option_enabled(self):
if TortoiseHelper.is_special_branch(self.repoBranch.get()) and self.optionCommit.get() == 0:
self.chkMergeProduction['state'] = Tkinter.DISABLED
self.optionMergeProduction.set(0)
else:
self.chkMergeProduction['state'] = Tkinter.NORMAL
self.set_silent_merge_production_option_enabled()
def set_merge_test_option_enabled(self):
if TortoiseHelper.is_special_branch(self.repoBranch.get()) and self.optionCommit.get() == 0:
self.chkMergeTest['state'] = Tkinter.DISABLED
self.optionMergeTest.set(0)
else:
self.chkMergeTest['state'] = Tkinter.NORMAL
self.set_silent_merge_test_option_enabled()
def set_silent_merge_production_option_enabled(self):
if self.optionMergeProduction.get() == 1:
self.chkMergeProductionSilent['state'] = Tkinter.NORMAL
else:
self.optionSilentMergeProduction.set(0)
self.chkMergeProductionSilent['state'] = Tkinter.DISABLED
def set_silent_merge_test_option_enabled(self):
if self.optionMergeTest.get() == 1:
self.chkMergeTestSilent['state'] = Tkinter.NORMAL
else:
self.optionSilentMergeTest.set(0)
self.chkMergeTestSilent['state'] = Tkinter.DISABLED
@staticmethod
def validate_repo(path):
return TortoiseHelper.run_command('hg id -R ' + path)['return_code'] == 0
def write_output(self, value, tag=''):
self.txtOutput['state'] = Tkinter.NORMAL
if len(self.txtOutput.get(1.0, Tkinter.END)) > 1:
self.txtOutput.insert(Tkinter.END, '\n')
self.txtOutput.insert(Tkinter.END, value, tag)
self.txtOutput['state'] = Tkinter.DISABLED
def main():
helper = TortoiseHelper()
helper.mainloop()
if (__name__ == '__main__'):
main()
__init__
, where you very likely should be constructing everything for the class? \$\endgroup\$