diff --git a/.gitignore b/.gitignore index d15fa18..a54a35c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,7 @@ coverage.xml .mr.developer.cfg .project .pydevproject -.settings +.settings/ # Rope .ropeproject @@ -52,5 +52,3 @@ coverage.xml # Sphinx documentation docs/_build/ - -test.py diff --git a/AUTHORS b/AUTHORS index 2e599b7..65700c5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,2 @@ lzrak47 +jasee diff --git a/CodingStyle.md b/CodingStyle.md index beef28f..fbb1f54 100644 --- a/CodingStyle.md +++ b/CodingStyle.md @@ -2,9 +2,7 @@ For code written in Python 2.7, please follow the [PEP8](http://legacy.python.or For code written in C maybe for high performance, please follow [The Official Git Coding Style](https://github.com/git/git/blob/master/Documentation/CodingGuidelines) by Linus Torvalds. -If you want to write some unit testing, please write them in the 'unittest' dir. - -I don't like unit testing at all personally. I mean, if you really like it, do it, I won't oppose it. +If you want to write some unit tests, please write them in the 'src/tests' dir. Everything shoud be written in English, includes comments and documents. diff --git a/README.md b/README.md index fe9199f..eccb2d5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,23 @@ ##Overview -This is the git-in-python project. +This is the git-in-python project, for *nix environment. It aims to rewrite Git in Python, perhaps with some C code for high performance. -The file named 'git.py' is the entrance of the whole project. + +The [git.py](https://github.com/lizherui/git-in-python/blob/master/src/git.py) is the entrance of the whole project. + +Before starting, please read the [CodingStyle](https://github.com/lizherui/git-in-python/blob/master/CodingStyle.md) and the [Schedule](https://github.com/lizherui/git-in-python/blob/master/Schedule.md). + +Before running, make sure having installed all the 3rd-party packages: + + pip install -r requirements.txt + +Before contributing, make sure having passed all the unit tests: + + cd src/tests + ./run_all_tests.sh + + ##Why [The Official Git](https://github.com/git/git) written in C attracts hackers all over the world since created. @@ -18,7 +32,10 @@ So, curiosity drives me to look inside [Git](https://github.com/git/git) and rew This project takes me a lot of time. I have to work for company during the day so that I can only dev this project during a few hours at night. -##Requirement +##Target +Dev the core command of the official git such as 'init', 'add', 'commit', 'push', 'clone' that when we run git.py xxx, the result is the same as git xxx. Otherwise, there is something wrong maybe. + +##Contribution Rewrite Git in Python seems not something easy, so this project is not for C/Python/Git beginners. However, don't get frustrated, It's not that hard.You can contribute to this project step by step: @@ -30,9 +47,6 @@ However, don't get frustrated, It's not that hard.You can contribute to this pro 5. understand Git source code: [the official Git source code](https://github.com/git/git) 6. fork this project, fix bugs, add features, and even rebuild the architecture. -##Target -Dev the core command of the official git such as 'init', 'add', 'commit', 'push', 'clone' that when we run git.py xxx, the result is the same as git xxx. Otherwise, there is something wrong maybe. - ##Future Surely, It takes time, It takes patients. diff --git a/Schedule.md b/Schedule.md index d77ae9e..4254968 100644 --- a/Schedule.md +++ b/Schedule.md @@ -2,11 +2,19 @@ * init * add * commit +* rm +* log +* status +* branch +* reset +* checkout +* diff ###Doing * push ###TODO * clone -* log -* diff +* merge +* pull +* ...... diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..34bc775 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +termcolor >= 1.1.0 +pathspec >= 0.2.2 diff --git a/src/branch.py b/src/branch.py new file mode 100644 index 0000000..2e70bbe --- /dev/null +++ b/src/branch.py @@ -0,0 +1,50 @@ +''' +Created on Jun 24, 2014 + +@author: lzrak47 +''' +import os +import shutil + +from constants import HEAD_PATH, REF_HEADS_DIR +from utils import read_file, write_to_file + + +class Branch(object): + ''' + git branch + ''' + + def __init__(self): + ''' + Constructor + ''' + self.head_name = read_file(HEAD_PATH).strip('\n').rsplit('/', 1)[-1] + self.head_path = os.path.join(REF_HEADS_DIR, self.head_name) + self.head_commit = read_file(self.head_path).strip() if os.path.exists(self.head_path) else None + + def get_all_branches(self): + return os.listdir(REF_HEADS_DIR) + + def _check_branch_exists(self, name): + return os.path.exists(os.path.join(REF_HEADS_DIR, name)) + + def add_branch(self, name): + if self._check_branch_exists(name): + print "fatal: A branch named '%s' already exists." % (name) + exit(1) + shutil.copyfile(self.head_path, os.path.join(REF_HEADS_DIR, name)) + + def delete_branch(self, name): + if self.head_name == name: + print "error: Cannot delete the branch '%s' which you are currently on." % (name) + exit(1) + os.remove(os.path.join(REF_HEADS_DIR, name)) + + def switch_branch(self, name): + if not self._check_branch_exists(name): + print "error: branch '%s' did not match any branches known to git." % (name) + exit(1) + write_to_file(HEAD_PATH, 'ref: refs/heads/%s' % name) + + \ No newline at end of file diff --git a/src/command.py b/src/command.py index 008bd35..4a3e202 100644 --- a/src/command.py +++ b/src/command.py @@ -5,7 +5,12 @@ ''' import os +from termcolor import colored + +from branch import Branch +from constants import GIT_DIR from repository import Repository +from utils import get_all_files_in_dir, filter_by_gitignore, less_str class Command(object): @@ -15,26 +20,79 @@ class Command(object): ''' @staticmethod - def cmd_init(workspace, bare): - Repository.create_repository(workspace, bare) + def cmd_init(workspace): + Repository.create_repository(workspace) @staticmethod - def cmd_add(workspace, file): + def cmd_add(file): if file == '.': - file_list = [] - for root, dirs, files in os.walk('.'): - if ".git" in dirs: - dirs.remove('.git') - for file in files: - file_list.append(os.path.join(root[2:], file)) - Repository(workspace).stage(file_list) + Repository().stage(filter_by_gitignore(get_all_files_in_dir('.', GIT_DIR))) else: - Repository(workspace).stage([file]) + Repository().stage([file]) @staticmethod - def cmd_commit(workspace, msg): - Repository(workspace).commit(msg) + def cmd_rm(file, cached=False): + Repository().delete(file) + if not cached: + os.remove(file) + + @staticmethod + def cmd_commit(msg): + Repository().commit(msg) + @staticmethod + def cmd_log(num, use_less=True): + res = Repository().show_log(num) + if use_less: + less_str(res) + else: + print res + + @staticmethod + def cmd_status(): + Repository().show_status() + + @staticmethod + def cmd_branch(name, is_deleted=False): + b = Branch() + if not name: + for branch in b.get_all_branches(): + print '* %s' % colored(branch, 'green') if branch == b.head_name else ' %s' % branch + elif is_deleted: + b.delete_branch(name) + else : + b.add_branch(name) + + @staticmethod + def cmd_reset(commit_sha1, is_soft=False, is_hard=False): + repo = Repository() + pre_entries = dict(repo.index.entries) + repo.update_head_commit(commit_sha1) + if not is_soft: + repo.rebuild_index_from_commit(commit_sha1) + if is_hard: + repo.rebuild_working_tree(pre_entries) + + @staticmethod + def cmd_checkout(branch): + b = Branch() + b.switch_branch(branch) + repo = Repository() + pre_entries = dict(repo.index.entries) + repo.rebuild_index_from_commit(repo.branch.head_commit) + repo.rebuild_working_tree(pre_entries) + + @staticmethod + def cmd_diff(cached=False, use_less=True): + if cached: + res = Repository().diff_between_index_and_head_tree() + else: + res = Repository().diff_between_working_tree_and_index() + if use_less: + less_str(res) + else: + print res + @staticmethod def cmd_push(): pass diff --git a/src/config.py b/src/config.py index cb3e9c1..170fe39 100644 --- a/src/config.py +++ b/src/config.py @@ -5,26 +5,27 @@ ''' import os + +from constants import CONFIG_PATH from utils import read_file + class Config(object): ''' config file ''' - - def __init__(self, workspace): + def __init__(self): ''' Constructor ''' - self.workspace = workspace self.config_dict = {} - paths = ['/etc/config', os.path.expanduser('~') + '/.gitconfig', os.path.join(workspace, '.git', 'config')] + paths = ['/etc/gitconfig', os.path.expanduser('~') + '/.gitconfig', CONFIG_PATH] for path in paths: if os.path.exists(path): self._parse_config_to_dict(path) - - + + def _parse_config_to_dict(self, path): content = read_file(path) for entry in content.split('[')[1:]: @@ -36,22 +37,15 @@ def _parse_config_to_dict(self, path): key = key_val.split(' = ')[0].strip() val = key_val.split(' = ')[1].strip() self.config_dict[index][key] = val - - + + @staticmethod def create_config(config_dict): str = '' for index, key_value in config_dict.iteritems(): str += '[%s]\n' % (index) for key, value in key_value.iteritems(): - str +='\t%s = %s\n' % (key, value) + str += '\t%s = %s\n' % (key, value) return str - - - - - - - - - \ No newline at end of file + + diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..b4a013f --- /dev/null +++ b/src/constants.py @@ -0,0 +1,52 @@ +''' +Created on Jun 19, 2014 + +@author: lzrak47 +''' + +import os + +GIT_DIR = '.git' + +INDEX_PATH = os.path.join(GIT_DIR, 'index') + +HEAD_PATH = os.path.join(GIT_DIR, 'HEAD') + +CONFIG_PATH = os.path.join(GIT_DIR, 'config') + +GITIGNORE_PATH = '.gitignore' + +DESCRIPTION_PATH = os.path.join(GIT_DIR, 'description') + +BRANCHES_DIR = os.path.join(GIT_DIR, 'branches') + +HOOK_DIR = os.path.join(GIT_DIR, 'hook') + +OBJECT_DIR = os.path.join(GIT_DIR, 'objects') +OBJECT_INFO_DIR = os.path.join(OBJECT_DIR, 'info') +OBJECT_PACK_DIR = os.path.join(OBJECT_DIR, 'pack') + +INFO_DIR = os.path.join(GIT_DIR, 'info') +INFO_EXCLUDE_PATH = os.path.join(INFO_DIR, 'exclude') + +REF_DIR = os.path.join(GIT_DIR, 'refs') +REF_HEADS_DIR = os.path.join(REF_DIR, 'heads') +REF_TAG_DIR = os.path.join(REF_DIR, 'tag') + +INIT_DIR = [ + BRANCHES_DIR, + HOOK_DIR, + INFO_DIR, + OBJECT_DIR, + OBJECT_PACK_DIR, + OBJECT_INFO_DIR, + REF_DIR, + REF_HEADS_DIR, + REF_TAG_DIR, +] + +INIT_FILE = [ + [HEAD_PATH, 'ref: refs/heads/master'], + [DESCRIPTION_PATH, 'Unnamed repository'], + [INFO_EXCLUDE_PATH, ''], +] diff --git a/src/git.py b/src/git.py index ea1a9b4..69a85a1 100755 --- a/src/git.py +++ b/src/git.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- ''' Created on Jun 8, 2014 @@ -7,7 +7,6 @@ ''' import argparse -import os import sys from command import Command @@ -31,18 +30,9 @@ def __init__(self, argv): { 'help' : 'Directory of the git repository', 'nargs' : '?', - 'default' : '', + 'default' : './', }, }, - - { - 'name' : ['--bare'], - 'properties' : - { - 'help' : 'Create a bare repository', - 'action' : 'store_true', - } - } ] }, @@ -59,10 +49,32 @@ def __init__(self, argv): }, ] }, + + 'rm' : { + 'func' : self._rm, + 'help' : 'Remove files from the working tree and from the index', + 'args' : [ + { + 'name' : ['file'], + 'properties' : + { + 'help' : 'Files to remove', + }, + }, + { + 'name' : ['--cached'], + 'properties' : + { + 'help' : 'Remove files only from the index', + 'action' : 'store_true', + }, + }, + ] + }, 'commit' : { 'func' : self._commit, - 'help' : 'Record changes to the repositoryx', + 'help' : 'Record changes to the repository', 'args' : [ { 'name' : ['-m', '--message'], @@ -75,6 +87,110 @@ def __init__(self, argv): ], }, + 'log' : { + 'func' : self._log, + 'help' : 'Show commit logs', + 'args' : [ + { + 'name' : ['-n'], + 'properties' : + { + 'help' : 'Limit the number of commits to output', + 'nargs' : '?', + 'type' : int, + 'dest' : 'num', + 'default' : float('infinity'), + } + }, + ], + }, + + 'status' : { + 'func' : self._status, + 'help' : 'Show the working tree status', + 'args' : [], + }, + + 'branch' : { + 'func' : self._branch, + 'help' : 'List, create, or delete branches', + 'args' : [ + { + 'name' : ['name'], + 'properties' : + { + 'help' : 'The name of the branch to create or delete', + 'nargs' : '?', + 'default' : '', + } + }, + { + 'name' : ['-d'], + 'properties' : + { + 'help' : 'Delete a branch.', + 'action' : 'store_true', + 'dest' : 'is_deleted', + } + } + ], + }, + 'reset' : { + 'func' : self._reset, + 'help' : 'Reset current HEAD to the specified state', + 'args' : [ + { + 'name' : ['commit_sha1'], + 'properties' : + { + 'help' : 'Commit to reset', + } + }, + { + 'name' : ['--soft'], + 'properties' : + { + 'help' : 'Does not touch the index file or the working tree at all', + 'action' : 'store_true', + } + }, + { + 'name' : ['--hard'], + 'properties' : + { + 'help' : 'Resets the index and working tree', + 'action' : 'store_true', + } + }, + ], + }, + 'checkout' : { + 'func' : self._checkout, + 'help' : 'Checkout a branch to the working tree', + 'args' : [ + { + 'name' : ['branch'], + 'properties' : + { + 'help' : 'Branch to checkout', + } + }, + ], + }, + 'diff' : { + 'func' : self._diff, + 'help' : 'Show changes between commits, commit and working tree, etc', + 'args' : [ + { + 'name' : ['--cached'], + 'properties' : + { + 'help' : 'Show changes between and the index and the head tree', + 'action' : 'store_true', + } + }, + ], + }, 'push' : { 'func' : self._push, 'help' : 'Update remote refs along with associated objects', @@ -89,15 +205,35 @@ def __init__(self, argv): } def _init(self, args): - workspace = os.path.join(os.getcwd(), args.directory) - Command.cmd_init(workspace=workspace, bare=args.bare) + Command.cmd_init(workspace=args.directory) def _add(self, args): - Command.cmd_add(os.getcwd(), args.file) + Command.cmd_add(args.file) + + def _rm(self, args): + Command.cmd_rm(args.file, args.cached) def _commit(self, args): - Command.cmd_commit(os.getcwd(), args.msg) + Command.cmd_commit(args.msg) + def _log(self, args): + Command.cmd_log(args.num) + + def _status(self, args): + Command.cmd_status() + + def _branch(self, args): + Command.cmd_branch(args.name, args.is_deleted) + + def _reset(self, args): + Command.cmd_reset(args.commit_sha1, is_soft=args.soft, is_hard=args.hard) + + def _checkout(self, args): + Command.cmd_checkout(args.branch) + + def _diff(self, args): + Command.cmd_diff(args.cached) + def _push(self, args): pass diff --git a/src/index.py b/src/index.py index 223f3d2..2a73007 100644 --- a/src/index.py +++ b/src/index.py @@ -10,7 +10,7 @@ import struct from objects import Tree -from utils import Sha1Reader, Sha1Writer, write_object_to_file +from utils import Sha1Reader, Sha1Writer, write_to_file class Index(object): @@ -33,7 +33,7 @@ def _parse_header(self, f): return entries_num - def add_entry(self, name, **kwargs): + def set_entry(self, name, **kwargs): self.entries[name] = kwargs def _parse_entries(self, f): @@ -46,7 +46,7 @@ def _parse_entries(self, f): real_size = ((f.tell() - begin + 8) & ~7) f.read((begin + real_size) - f.tell()) - self.add_entry(name, ctime=ctime, mtime=mtime, dev=dev, ino=ino, mode=mode, \ + self.set_entry(name, ctime=ctime, mtime=mtime, dev=dev, ino=ino, mode=int(mode), \ uid=uid, gid=gid, size=size,sha1=binascii.hexlify(sha1), flags=flags & ~0x0fff) def _parse_file(self): @@ -96,7 +96,7 @@ def write_to_file(self): os.rename(lock_file, self.path) - def do_commit(self, workspace): + def do_commit(self): tree = {} for path, property in self.entries.iteritems(): t = tree @@ -115,23 +115,9 @@ def _build_tree(path): else: (mode, sha1) = entry file_arr.append({'name':name, 'mode':mode, 'sha1':sha1}) - newtree = Tree(workspace, sorted(dir_arr,key = lambda x:x['name']) + sorted(file_arr,key = lambda x:x['name'])) - write_object_to_file(newtree.path, newtree.content) + newtree = Tree(sorted(dir_arr,key = lambda x:x['name']) + sorted(file_arr,key = lambda x:x['name'])) + write_to_file(newtree.path, newtree.content) return newtree return _build_tree(tree) - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/objects.py b/src/objects.py index bb4c25c..a52c9e5 100644 --- a/src/objects.py +++ b/src/objects.py @@ -6,67 +6,93 @@ import binascii import os +import re +import stat import zlib -from utils import cal_sha1 +from constants import OBJECT_DIR +from utils import cal_sha1, read_file + class BaseObject(object): ''' git base object ''' - def __init__(self, workspace, content): + def __init__(self, final_content=None, sha1=None): ''' Constructor ''' - self.content = zlib.compress(content) - self.sha1 = cal_sha1(content) - self.path = os.path.join(workspace, '.git', 'objects', self.sha1[:2], self.sha1[2:]) + if sha1: + self.sha1 = sha1 + self.path = os.path.join(OBJECT_DIR, self.sha1[:2], self.sha1[2:]) + self.content = read_file(self.path) + else: + self.content = zlib.compress(final_content) + self.sha1 = cal_sha1(final_content) + self.path = os.path.join(OBJECT_DIR, self.sha1[:2], self.sha1[2:]) class Blob(BaseObject): - def __init__(self, workspace, content): - real_content = 'blob %d\0%s' % (len(content), content) - super(Blob, self).__init__(workspace, real_content) + def __init__(self, raw_content=None, sha1=None): + if sha1: + super(Blob, self).__init__(sha1=sha1) + final_content = zlib.decompress(self.content) + self.raw_content = final_content[final_content.find('\0') + 1:] + else: + final_content = 'blob %d\0%s' % (len(raw_content), raw_content) + super(Blob, self).__init__(final_content) class Tree(BaseObject): - def __init__(self, workspace, args): - content = '' - for arg in args: - content += '%04o %s\0%s' % (arg['mode'], arg['name'], binascii.unhexlify(arg['sha1'])) - real_content = 'tree %d\0%s' % (len(content), content) - super(Tree, self).__init__(workspace, real_content) + def __init__(self, args=None, sha1=None): + if sha1: + super(Tree, self).__init__(sha1=sha1) + final_content = zlib.decompress(self.content) + self.raw_content = final_content[final_content.find('\0') + 1:] + self.objects = re.findall('(\d+) ([^\0]+)\0(.{20})', self.raw_content, re.S) + + else: + raw_content = '' + for arg in args: + raw_content += '%04o %s\0%s' % (arg['mode'], arg['name'], binascii.unhexlify(arg['sha1'])) + final_content = 'tree %d\0%s' % (len(raw_content), raw_content) + super(Tree, self).__init__(final_content) + + def parse_objects(self): + res = {} + queue = list(self.objects) + while queue: + object = queue.pop(0) + mode, name, sha1 = object[0], object[1], binascii.hexlify(object[2]) + if stat.S_ISDIR(int(mode, 8)): + new_objects = Tree(sha1=sha1).objects + for new_object in new_objects: + queue.append([new_object[0], os.path.join(name, new_object[1]), new_object[2]]) + else: + res[name] = {'mode' : int(mode, 8), 'sha1' : sha1} + return res class Commit(BaseObject): - def __init__(self, workspace, **kwargs): - content = 'tree %s\n' % (kwargs['tree_sha1']) - if kwargs['parent_sha1']: - content += 'parent %s\n' % (kwargs['parent_sha1']) + def __init__(self, **kwargs): + if kwargs['sha1']: + super(Commit, self).__init__(sha1=kwargs['sha1']) + final_content = zlib.decompress(self.content) + res = re.findall('parent (\w+)\n', final_content) + self.parent_sha1 = res[0] if res else None + self.raw_content = final_content[final_content.find('tree'):] + self.tree = re.findall('tree (\w+)\n', final_content)[0] - content += 'author %s %s %s %s\ncommitter %s %s %s %s\n\n%s\n' \ - % (kwargs['name'], kwargs['email'], kwargs['timestamp'], kwargs['timezone'] , \ - kwargs['name'], kwargs['email'], kwargs['timestamp'], kwargs['timezone'] , kwargs['msg']) - - real_content = 'commit %d\0%s' % (len(content), content) - super(Commit, self).__init__(workspace, real_content) + else: + raw_content = 'tree %s\n' % (kwargs['tree_sha1']) + if kwargs['parent_sha1']: + raw_content += 'parent %s\n' % (kwargs['parent_sha1']) + + raw_content += 'author %s %s %s %s\ncommitter %s %s %s %s\n\n%s\n' \ + % (kwargs['name'], kwargs['email'], kwargs['timestamp'], kwargs['timezone'] , \ + kwargs['name'], kwargs['email'], kwargs['timestamp'], kwargs['timezone'] , kwargs['msg']) - - - - - - - - - - - - - - - - - + final_content = 'commit %d\0%s' % (len(raw_content), raw_content) + super(Commit, self).__init__(final_content) \ No newline at end of file diff --git a/src/repository.py b/src/repository.py index fc2fe12..58d83ee 100644 --- a/src/repository.py +++ b/src/repository.py @@ -6,104 +6,266 @@ import os import time +from termcolor import colored + +from branch import Branch from config import Config +from constants import INDEX_PATH, GIT_DIR, INIT_DIR, \ + INIT_FILE, CONFIG_PATH from index import Index -from objects import Blob, Commit -from utils import read_file, write_to_file, cal_mode, write_object_to_file +from objects import Blob, Commit, Tree +from utils import get_all_files_in_dir, read_file, write_to_file, cal_mode, \ + less_str, filter_by_gitignore, get_file_mode, diff_file class Repository(object): ''' The git repository ''' + def __init__(self): + self.index = Index(INDEX_PATH) + self.working_tree_files = get_all_files_in_dir('.', GIT_DIR) + self.config = Config() + self.branch = Branch() - GIT_DIR = '.git' - - INIT_DIR = [ - 'branches', - 'hooks', - 'info', - 'objects', - 'objects/info', - 'objects/pack', - 'refs', - 'refs/heads', - 'refs/tags', - ] - - INIT_FILE = [ - ['HEAD', 'ref: refs/heads/master'], - ['description', 'Unnamed repository'], - ['info/exclude', ''], - ] - - - def __init__(self, workspace): - self.workspace = workspace - self.index = Index(os.path.join(workspace, '.git', 'index')) - self.config = Config(workspace) - def stage(self, files): try: for file in files: content = read_file(file) - blob = Blob(self.workspace, content) + blob = Blob(content) if not os.path.exists(blob.path): - write_object_to_file(blob.path, blob.content) - stat = os.stat(os.path.join(self.workspace, file)) - self.index.add_entry(file, ctime=stat.st_ctime, mtime=stat.st_mtime, dev=stat.st_dev, ino=stat.st_ino, mode=cal_mode(stat.st_mode), \ - uid=stat.st_uid, gid=stat.st_gid, size=stat.st_size,sha1=blob.sha1, flags=0) + write_to_file(blob.path, blob.content) + stat = os.stat(os.path.join(file)) + self.index.set_entry(file, ctime=stat.st_ctime, mtime=stat.st_mtime, dev=stat.st_dev, ino=stat.st_ino, mode=cal_mode(stat.st_mode), \ + uid=stat.st_uid, gid=stat.st_gid, size=stat.st_size, sha1=blob.sha1, flags=0) self.index.write_to_file() - + self.index = Index(INDEX_PATH) + except Exception, e: print 'stage file %s error: %s' % (file, e) - - @staticmethod - def create_repository(workspace, bare=False): - if not os.path.exists(workspace): + + @staticmethod + def create_repository(workspace): + if not os.path.exists(workspace): os.mkdir(workspace) os.chdir(workspace) - - if not bare: - os.mkdir(Repository.GIT_DIR) - os.chdir(Repository.GIT_DIR) - - for new_dir in Repository.INIT_DIR: - os.mkdir(new_dir) - - for file_and_content in Repository.INIT_FILE: - file_name = file_and_content[0] - content = file_and_content[1] - write_to_file(file_name, content) - - + + if not os.path.exists(GIT_DIR): + os.mkdir(GIT_DIR) + + for new_dir in INIT_DIR: + os.mkdir(new_dir) + + for file_and_content in INIT_FILE: + file_name = file_and_content[0] + content = file_and_content[1] + write_to_file(file_name, content) + + init_config_dict = { 'core': { 'repositoryformatversion' : '0', 'filemode' : 'true', - 'bare' : str(bare).lower(), + 'bare' : 'true', 'logallrefupdates' : 'true', } } - + content = Config.create_config(init_config_dict) - write_to_file('config', content) - - def commit(self, msg, ref='HEAD'): - cur_tree = self.index.do_commit(self.workspace) - branch_name = read_file(os.path.join(self.workspace, '.git', 'HEAD')).strip('\n').rsplit('/', 1)[-1] - ref_path = os.path.join(self.workspace, '.git', 'refs', 'heads', branch_name) - parent_sha1 = None - if os.path.exists(ref_path): - parent_sha1 = read_file(ref_path) + write_to_file(CONFIG_PATH, content) + + def commit(self, msg): + new_tree = self.index.do_commit() + committer_name = self.config.config_dict['user']['name'] - committer_email = '<%s>' % (self.config.config_dict['user']['email']) + committer_email = '<%s>' % (self.config.config_dict['user']['email']) commit_time = int(time.time()) - - #TO FIX commit_timezone = time.strftime("%z", time.gmtime()) - - commit = Commit(self.workspace, tree_sha1=cur_tree.sha1, parent_sha1=parent_sha1, name=committer_name, email=committer_email, \ + + commit = Commit(sha1=None, tree_sha1=new_tree.sha1, parent_sha1=self.branch.head_commit, name=committer_name, email=committer_email, \ timestamp=commit_time, timezone=commit_timezone, msg=msg) - write_object_to_file(commit.path, commit.content) - write_to_file(ref_path, commit.sha1) \ No newline at end of file + write_to_file(commit.path, commit.content) + write_to_file(self.branch.head_path, commit.sha1) + + def delete(self, file): + del self.index.entries[file] + self.index.write_to_file() + + def show_log(self, num): + cur_commit = Commit(sha1=self.branch.head_commit) + print_str = cur_commit.raw_content + while num > 1 and cur_commit.parent_sha1: + num -= 1 + parent_commit = Commit(sha1=cur_commit.parent_sha1) + print_str += '\n%s' % (parent_commit.raw_content) + cur_commit = parent_commit + return print_str + + def get_untracked_files(self): + raw_list = list(set(self.working_tree_files).difference(set(list(self.index.entries)))) + return filter_by_gitignore(raw_list) + + def get_unstaged_files(self): + res = { + 'modified': [], + 'deleted' : [], + } + for name, properties in self.index.entries.iteritems(): + if name not in self.working_tree_files: + res['deleted'].append(name) + elif get_file_mode(name) != properties['mode'] or Blob(read_file(name)).sha1 != properties['sha1']: + res['modified'].append(name) + + return res + + + def get_uncommitted_files(self): + if not self.branch.head_commit: + return { + 'modified' : [], + 'deleted' : [], + 'new file' : self.index.entries, + } + + tree = Tree(sha1=Commit(sha1=self.branch.head_commit).tree) + tree_objects = tree.parse_objects() + return { + 'modified': [name for name in set(self.index.entries).intersection(set(tree_objects)) \ + if self.index.entries[name]['sha1'] != tree_objects[name]['sha1'] or \ + self.index.entries[name]['mode'] != tree_objects[name]['mode']], + 'deleted' : list(set(tree_objects).difference(self.index.entries)), + 'new file' : list(set(self.index.entries).difference(set(tree_objects))), + } + + def show_status(self): + untracked_files = self.get_untracked_files() + unstaged_files = self.get_unstaged_files() + uncommitted_files = self.get_uncommitted_files() + print_str = 'On branch %s\n' % (self.branch.head_name) + + if uncommitted_files['modified'] or uncommitted_files['deleted'] or uncommitted_files['new file']: + print_str += 'Changes to be committed:\n (use "git reset HEAD ..." to unstage)\n\n' + for change, files in uncommitted_files.iteritems(): + for file in files: + print_str += colored('\t%s:\t%s\n' % (change, file), 'green') + print_str += '\n' + else: + print_str += '\nno changes added to commit\n\n' + + + if unstaged_files['modified'] or unstaged_files['deleted']: + print_str += 'Changes not staged for commit:\n (use "git add ..." to update what will be committed)\n' + print_str += ' (use "git checkout -- ..." to discard changes in working directory)\n\n' + for change, files in unstaged_files.iteritems(): + for file in files: + print_str += colored('\t%s:\t%s\n' % (change, file), 'red') + print_str += '\n' + + + if untracked_files: + print_str += 'Untracked files:\n (use "git add ..." to include in what will be committed)\n\n' + for file in untracked_files: + print_str += colored('\t%s\n' % file, 'red') + print_str += '\n' + + print print_str + + def update_head_commit(self, commit_sha1): + write_to_file(self.branch.head_path, commit_sha1) + + def rebuild_index_from_commit(self, commit_sha1): + tree = Tree(sha1=Commit(sha1=commit_sha1).tree) + tree_objects = tree.parse_objects() + + for name in set(self.index.entries).difference(set(tree_objects)): + self.index.entries.pop(name) + + for name, properties in tree_objects.iteritems(): + if not self.index.entries.has_key(name) or properties['sha1'] != self.index.entries[name]['sha1'] or \ + properties['mode'] != self.index.entries[name]['mode']: + self.index.set_entry(name, ctime=0.0, mtime=0.0, dev=0, ino=0, mode=properties['mode'], \ + uid=0, gid=0, size=0, sha1=properties['sha1'], flags=0) + + self.index.write_to_file() + self.index = Index(INDEX_PATH) + + def rebuild_working_tree(self, pre_entries): + for name in set(pre_entries).difference(set(self.index.entries)): + os.remove(name) + + for path, properties in self.index.entries.iteritems(): + content = Blob(sha1=properties['sha1']).raw_content + write_to_file(path, content, mode=properties['mode']) + + + def diff_between_working_tree_and_index(self): + res = '' + for path in self.get_unstaged_files()['modified']: + old_file = { + 'path' : path, + 'sha1' : self.index.entries[path]['sha1'], + 'mode' : self.index.entries[path]['mode'], + 'content' : Blob(sha1=self.index.entries[path]['sha1']).raw_content, + } + + f = read_file(path) + blob = Blob(f) + new_file = { + 'path' : path, + 'sha1' : blob.sha1, + 'mode' : get_file_mode(path), + 'content' : f, + } + res += diff_file(old_file, new_file) + for path in self.get_unstaged_files()['deleted']: + old_file = { + 'path' : path, + 'sha1' : self.index.entries[path]['sha1'], + 'mode' : self.index.entries[path]['mode'], + 'content' : Blob(sha1=self.index.entries[path]['sha1']).raw_content, + } + new_file = {'path':None, 'sha1':'0' * 7, 'mode': None, 'content':'',} + res += diff_file(old_file, new_file) + return res + + def diff_between_index_and_head_tree(self): + tree = Tree(sha1=Commit(sha1=self.branch.head_commit).tree) + tree_objects = tree.parse_objects() + res = '' + for path in self.get_uncommitted_files()['modified']: + old_file = { + 'path' : path, + 'sha1' : tree_objects[path]['sha1'], + 'mode' : tree_objects[path]['mode'], + 'content' : Blob(sha1=tree_objects[path]['sha1']).raw_content, + } + + new_file = { + 'path' : path, + 'sha1' : self.index.entries[path]['sha1'], + 'mode' : self.index.entries[path]['mode'], + 'content' : Blob(sha1=self.index.entries[path]['sha1']).raw_content, + } + res += diff_file(old_file, new_file) + + for path in self.get_uncommitted_files()['deleted']: + old_file = { + 'path' : path, + 'sha1' : tree_objects[path]['sha1'], + 'mode' : tree_objects[path]['mode'], + 'content' : Blob(sha1=tree_objects[path]['sha1']).raw_content, + } + new_file = {'path':None, 'sha1':'0' * 7, 'mode': None, 'content':'',} + res += diff_file(old_file, new_file) + + for path in self.get_uncommitted_files()['new file']: + old_file = {'path':None, 'sha1':'0' * 7, 'mode': None, 'content':'',} + new_file = { + 'path' : path, + 'sha1' : self.index.entries[path]['sha1'], + 'mode' : self.index.entries[path]['mode'], + 'content' : Blob(sha1=self.index.entries[path]['sha1']).raw_content, + } + res += diff_file(old_file, new_file) + return res + \ No newline at end of file diff --git a/unittest/__init__.py b/src/tests/__init__.py similarity index 100% rename from unittest/__init__.py rename to src/tests/__init__.py diff --git a/src/tests/run_all_tests.sh b/src/tests/run_all_tests.sh new file mode 100755 index 0000000..f47f61b --- /dev/null +++ b/src/tests/run_all_tests.sh @@ -0,0 +1,2 @@ +export PYTHONPATH=..:$PYTHONPATH +python -m unittest discover -v diff --git a/src/tests/test_add.py b/src/tests/test_add.py new file mode 100644 index 0000000..88ec7fe --- /dev/null +++ b/src/tests/test_add.py @@ -0,0 +1,59 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from command import Command +from objects import Blob +from repository import Repository +from utils import write_to_file, get_file_mode + + +class TestAdd(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_add' + Command.cmd_init(self.workspace) + + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + def _check_blob_and_index(self, *paths_contents): + entries = Repository().index.entries + for path, content in paths_contents: + sha1 = Blob(content).sha1 + blob = Blob(sha1=sha1) + self.assertEqual(blob.raw_content, content) + self.assertEqual(entries[path]['sha1'], sha1) + self.assertEqual(entries[path]['mode'], get_file_mode(path)) + + def test_add_file(self): + path = '1.txt' + content = '1\n' + write_to_file(path, content) + Command.cmd_add(path) + self._check_blob_and_index((path, content)) + + def test_add_dir(self): + path = os.path.join('dir', '2.txt') + content = '2\n' + write_to_file(path, content) + Command.cmd_add(path) + self._check_blob_and_index((path, content)) + + def test_add_all(self): + paths_contents = [('1.txt', '1\n'), (os.path.join('dir', '2.txt'), '2\n')] + for path, content in paths_contents: + write_to_file(path, content) + Command.cmd_add('.') + self._check_blob_and_index(*paths_contents) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/tests/test_branch.py b/src/tests/test_branch.py new file mode 100644 index 0000000..50db002 --- /dev/null +++ b/src/tests/test_branch.py @@ -0,0 +1,46 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from branch import Branch +from command import Command + + +class TestBranch(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_branch' + Command.cmd_init(self.workspace) + Command.cmd_commit('first ci') + self.new_branch = 'new_branch' + + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + + def test_branch_add(self): + Command.cmd_branch(self.new_branch) + self.assertIn(self.new_branch, Branch().get_all_branches()) + + def test_branch_delete(self): + Command.cmd_branch(self.new_branch) + self.assertIn(self.new_branch, Branch().get_all_branches()) + Command.cmd_branch(self.new_branch, True) + self.assertNotIn(self.new_branch, Branch().get_all_branches()) + + def test_branch_list(self): + Command.cmd_branch('') + Command.cmd_branch(self.new_branch) + Command.cmd_branch('') + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/tests/test_checkout.py b/src/tests/test_checkout.py new file mode 100644 index 0000000..4d4bb74 --- /dev/null +++ b/src/tests/test_checkout.py @@ -0,0 +1,56 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from branch import Branch +from command import Command +from utils import write_to_file, read_file + + +class TestCheckout(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_branch' + Command.cmd_init(self.workspace) + Command.cmd_commit('first ci') + self.file_list = [('1.txt', '1\n'), ('2.txt', '2\n')] + for path, content in self.file_list: + write_to_file(path, content) + Command.cmd_add(path) + Command.cmd_commit('master ci') + + self.new_branch = 'new_branch' + Command.cmd_branch(self.new_branch) + + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + + def test_checkout(self): + Command.cmd_checkout(self.new_branch) + self.assertEqual(Branch().head_name, self.new_branch) + + write_to_file(self.file_list[0][0], '11\n') + Command.cmd_rm(self.file_list[1][0]) + new_path = '3.txt' + new_content = '3\n' + write_to_file(new_path, new_content) + Command.cmd_add('.') + Command.cmd_commit('branch ci') + + Command.cmd_checkout('master') + self.assertTrue(os.path.exists(self.file_list[1][0])) + self.assertFalse(os.path.exists(new_path)) + self.assertEqual(read_file(self.file_list[0][0]), self.file_list[0][1]) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/tests/test_commit.py b/src/tests/test_commit.py new file mode 100644 index 0000000..4cba785 --- /dev/null +++ b/src/tests/test_commit.py @@ -0,0 +1,64 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from branch import Branch +from command import Command +from objects import Commit, Tree, Blob +from utils import write_to_file + + +class TestCommit(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_commit' + Command.cmd_init(self.workspace) + self.path = '1.txt' + self.content = '1\n' + write_to_file(self.path, self.content) + Command.cmd_add(self.path) + + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + + def test_commit_once(self): + Command.cmd_commit('first ci') + commit = Commit(sha1=Branch().head_commit) + self.assertIsNone(commit.parent_sha1) + tree = Tree(sha1=commit.tree) + objects = tree.parse_objects() + self.assertEqual(objects[self.path]['sha1'], Blob(self.content).sha1) + + def test_commit_twice(self): + Command.cmd_commit('first ci') + parent_sha1 = Branch().head_commit + + second_content = '11\n' + write_to_file(self.path, second_content) + + new_path = '2.txt' + new_content = '2\n' + write_to_file(new_path, new_content) + + Command.cmd_add('.') + Command.cmd_commit('second ci') + + commit = Commit(sha1=Branch().head_commit) + self.assertEqual(parent_sha1, commit.parent_sha1) + tree = Tree(sha1=commit.tree) + objects = tree.parse_objects() + self.assertEqual(objects[self.path]['sha1'], Blob(second_content).sha1) + self.assertEqual(objects[new_path]['sha1'], Blob(new_content).sha1) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/tests/test_diff.py b/src/tests/test_diff.py new file mode 100644 index 0000000..e12ff4c --- /dev/null +++ b/src/tests/test_diff.py @@ -0,0 +1,75 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from command import Command +from utils import write_to_file + + +class TestDiff(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_diff' + Command.cmd_init(self.workspace) + self.old_content = ''' + The Way that can be told of is not the eternal Way; + The name that can be named is not the eternal name. + The Nameless is the origin of Heaven and Earth; + The Named is the mother of all things. + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, + so we may see their outcome. + The two are the same, + But after they are produced, + they have different names. + ''' + self.new_content = ''' + The Nameless is the origin of Heaven and Earth; + The named is the mother of all things. + + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, + so we may see their outcome. + The two are the same, + But after they are produced, + they have different names. + They both may be called deep and profound. + Deeper and more profound, + The door of all subtleties! + ''' + self.file_list = [('1.txt', self.old_content), ('2.txt', self.old_content)] + for path, content in self.file_list: + write_to_file(path, content) + Command.cmd_add(path) + + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + + def test_diff_default(self): + write_to_file(self.file_list[0][0], self.new_content) + os.remove(self.file_list[1][0]) + Command.cmd_diff(False, False) + + def test_diff_cached(self): + Command.cmd_commit('first ci') + write_to_file(self.file_list[0][0], self.new_content) + Command.cmd_rm(self.file_list[1][0]) + new_path = '3.txt' + write_to_file(new_path, self.new_content) + Command.cmd_add('.') + Command.cmd_diff(True, False) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/tests/test_init.py b/src/tests/test_init.py new file mode 100644 index 0000000..baa9841 --- /dev/null +++ b/src/tests/test_init.py @@ -0,0 +1,52 @@ +''' +Created on Jul 2, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from command import Command +from constants import GIT_DIR, INIT_DIR, INIT_FILE +from utils import read_file + + +class TestInit(unittest.TestCase): + + def _check_dirs_and_files(self, workspace): + Command.cmd_init(workspace) + self.assertTrue(os.path.exists(GIT_DIR)) + for dir in INIT_DIR: + self.assertTrue(os.path.exists(dir)) + for file in INIT_FILE: + path = file[0] + content = file[1] + self.assertEqual(read_file(path), content) + + def test_init_with_workspace(self): + workspace = 'test_init' + self._check_dirs_and_files(workspace) + os.chdir('..') + shutil.rmtree(workspace) + + def test_init_without_workspace(self): + workspace = './' + self._check_dirs_and_files(workspace) + shutil.rmtree(GIT_DIR) + + def test_init_in_existing_repository(self): + workspace = "test" + Command.cmd_init(workspace) + os.chdir('..') + try: + Command.cmd_init(workspace) + except OSError: + self.assertEqual(1, 2, "Reinitialized existing repository failed") + finally: + os.chdir('..') + shutil.rmtree(workspace) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/tests/test_log.py b/src/tests/test_log.py new file mode 100644 index 0000000..c333ff8 --- /dev/null +++ b/src/tests/test_log.py @@ -0,0 +1,48 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from command import Command +from utils import write_to_file + + +class TestLog(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_log' + Command.cmd_init(self.workspace) + + self.path = '1.txt' + self.content = '1\n' + write_to_file(self.path, self.content) + + Command.cmd_add(self.path) + Command.cmd_commit('first ci') + + second_content = '11\n' + write_to_file(self.path, second_content) + + Command.cmd_add(self.path) + Command.cmd_commit('second ci') + + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + + def test_log_without_num(self): + Command.cmd_log(float('infinity'), False) + + def test_log_with_num(self): + Command.cmd_log(1, False) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/tests/test_reset.py b/src/tests/test_reset.py new file mode 100644 index 0000000..c796db7 --- /dev/null +++ b/src/tests/test_reset.py @@ -0,0 +1,69 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from branch import Branch +from command import Command +from repository import Repository +from utils import write_to_file, read_file + + +class Test(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_reset' + Command.cmd_init(self.workspace) + + self.path, self.content = ('1.txt', '1\n') + write_to_file(self.path, self.content) + Command.cmd_add(self.path) + Command.cmd_commit('first ci') + self.first_commit = Branch().head_commit + + write_to_file(self.path, '2.txt') + Command.cmd_add(self.path) + Command.cmd_commit('second ci') + + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + + def test_reset_soft(self): + Command.cmd_reset(self.first_commit, is_soft=True, is_hard=False) + self.assertEqual(Branch().head_commit, self.first_commit) + repo = Repository() + uncommitted_files = repo.get_uncommitted_files() + unstaged_files = repo.get_unstaged_files() + self.assertIn(self.path, uncommitted_files['modified']) + self.assertFalse(unstaged_files['modified']) + + def test_reset_default(self): + Command.cmd_reset(self.first_commit, is_soft=False, is_hard=False) + self.assertEqual(Branch().head_commit, self.first_commit) + repo = Repository() + uncommitted_files = repo.get_uncommitted_files() + unstaged_files = repo.get_unstaged_files() + self.assertFalse(uncommitted_files['modified']) + self.assertIn(self.path, unstaged_files['modified']) + + def test_reset_hard(self): + Command.cmd_reset(self.first_commit, is_soft=False, is_hard=True) + self.assertEqual(Branch().head_commit, self.first_commit) + repo = Repository() + uncommitted_files = repo.get_uncommitted_files() + unstaged_files = repo.get_unstaged_files() + self.assertFalse(uncommitted_files['modified']) + self.assertFalse(unstaged_files['modified']) + self.assertEqual(read_file(self.path), self.content) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/tests/test_rm.py b/src/tests/test_rm.py new file mode 100644 index 0000000..441e2cc --- /dev/null +++ b/src/tests/test_rm.py @@ -0,0 +1,49 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from command import Command +from repository import Repository +from utils import write_to_file + + +class TestRm(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_rm' + Command.cmd_init(self.workspace) + self.path = '1.txt' + content = '1\n' + write_to_file(self.path, content) + Command.cmd_add(self.path) + + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + + def test_rm_cached(self): + entries = Repository().index.entries + self.assertIn(self.path, entries) + Command.cmd_rm(self.path, True) + entries = Repository().index.entries + self.assertNotIn(self.path, entries) + + def test_rm_no_cached(self): + entries = Repository().index.entries + self.assertIn(self.path, entries) + Command.cmd_rm(self.path, False) + entries = Repository().index.entries + self.assertNotIn(self.path, entries) + self.assertFalse(os.path.exists(self.path)) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/tests/test_status.py b/src/tests/test_status.py new file mode 100644 index 0000000..7f3d968 --- /dev/null +++ b/src/tests/test_status.py @@ -0,0 +1,79 @@ +''' +Created on Jul 4, 2014 + +@author: lzrak47 +''' +import os +import shutil +import unittest + +from command import Command +from repository import Repository +from utils import write_to_file + + +class TestStatus(unittest.TestCase): + + + def setUp(self): + self.workspace = 'test_status' + Command.cmd_init(self.workspace) + + def tearDown(self): + os.chdir('..') + shutil.rmtree(self.workspace) + + def test_status_init(self): + repo = Repository() + untracked_files = repo.get_untracked_files() + self.assertFalse(untracked_files) + Command.cmd_status() + + def test_status_untracked_files(self): + path, content = ('1.txt', '1\n') + write_to_file(path, content) + repo = Repository() + untracked_files = repo.get_untracked_files() + self.assertEqual(untracked_files, ['1.txt']) + Command.cmd_status() + + def test_status_unstaged_files(self): + file_list = [('1.txt', '1\n'), ('2.txt', '2\n')] + for path, content in file_list: + write_to_file(path, content) + Command.cmd_add(path) + + write_to_file(file_list[0][0], '11\n') + os.remove(file_list[1][0]) + + repo = Repository() + unstaged_files = repo.get_unstaged_files() + + self.assertEqual(unstaged_files['modified'], [file_list[0][0]]) + self.assertEqual(unstaged_files['deleted'], [file_list[1][0]]) + Command.cmd_status() + + def test_status_uncommitted_files(self): + file_list = [('1.txt', '1\n'), ('2.txt', '2\n')] + for path, content in file_list: + write_to_file(path, content) + Command.cmd_add(path) + Command.cmd_commit('first ci') + + write_to_file(file_list[0][0], '11\n') + Command.cmd_rm(file_list[1][0]) + new_path = '3.txt' + new_content = '3\n' + write_to_file(new_path, new_content) + Command.cmd_add('.') + + repo = Repository() + uncommitted_files = repo.get_uncommitted_files() + self.assertEqual(uncommitted_files['modified'], [file_list[0][0]]) + self.assertEqual(uncommitted_files['deleted'], [file_list[1][0]]) + self.assertEqual(uncommitted_files['new file'], [new_path]) + Command.cmd_status() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/utils.py b/src/utils.py index 9f49609..cb540c8 100644 --- a/src/utils.py +++ b/src/utils.py @@ -4,34 +4,35 @@ @author: lzrak47 ''' +from difflib import unified_diff import hashlib import os import stat -import sys +import tempfile + +import pathspec +from termcolor import colored + +from constants import GITIGNORE_PATH S_IFGITLINK = 0o160000 -def write_to_file(path, content): - f = open(path, 'w') - f.write(content) - f.close() - -def write_object_to_file(path, content): +def write_to_file(path, content, mode=None): dir = os.path.dirname(path) - if not os.path.exists(dir): - os.mkdir(dir) - write_to_file(path, content) + + if dir and not os.path.exists(dir): + os.makedirs(dir) + + with open(path, 'w') as f: + f.write(content) + + if mode: + os.chmod(path, mode) def read_file(file_name): - try: - f = open(file_name, 'r') - content = f.read() - f.close() - return content - except Exception, e: - print "open file %s error: %s" % (file_name, e) - sys.exit(1) + with open(file_name, 'r') as f: + return f.read() def cal_sha1(content): sha1 = hashlib.sha1() @@ -49,6 +50,68 @@ def cal_mode(mode): ret |= (mode & 0o111) return ret +def get_file_mode(path): + res = os.stat(path) + return cal_mode(res.st_mode) + +def less_str(str): + with tempfile.NamedTemporaryFile() as f: + f.write(str) + f.seek(0) + os.system("cat %s | less" % f.name) + +def get_all_files_in_dir(dir, *exclude_dirs): + file_list = [] + for root, dirs, files in os.walk(dir): + for exclude_dir in set(exclude_dirs).intersection(set(dirs)): + dirs.remove(exclude_dir) + for file in files: + file_list.append(os.path.join(root[2:], file)) + return file_list + +def filter_by_gitignore(raw_list): + if not os.path.exists(GITIGNORE_PATH): + return raw_list + else: + with open(GITIGNORE_PATH, 'r') as fh: + spec = pathspec.PathSpec.from_lines(pathspec.GitIgnorePattern, fh) + return list(set(raw_list).difference(spec.match_files(raw_list))) + +def diff_file(old_file, new_file): + if old_file['path']: + print_str = 'diff --git a/%s b/%s\n' % (old_file['path'], old_file['path']) + else: + print_str = 'diff --git a/%s b/%s\n' % (new_file['path'], new_file['path']) + + if old_file['mode'] == new_file['mode']: + print_str += 'index %.7s..%.7s %04o\n' % (old_file['sha1'], new_file['sha1'], new_file['mode']) + + elif not old_file['mode']: + print_str += 'new file mode %04o\n' % (new_file['mode']) + print_str += 'index %.7s..%.7s\n' % (old_file['sha1'], new_file['sha1']) + + elif not new_file['mode']: + print_str += 'deleted file mode %04o\n' % (old_file['mode']) + print_str += 'index %.7s..%.7s\n' % (old_file['sha1'], new_file['sha1']) + + else: + print_str += 'old mode %04o\n' % (old_file['mode']) + print_str += 'new mode %04o\n' % (new_file['mode']) + print_str += 'index %.7s..%.7s\n' % (old_file['sha1'], new_file['sha1']) + + from_file = 'a/%s' % old_file['path'] if old_file['path'] else '/dev/null' + to_file = 'b/%s' % new_file['path'] if new_file['path'] else '/dev/null' + for i, line in enumerate(unified_diff(old_file['content'].splitlines(), new_file['content'].splitlines(), fromfile=from_file , tofile=to_file)): + str = '%s\n' % line.strip('\n') + if line.startswith('@@'): + print_str += colored(str, 'cyan') + elif line.startswith('+') and i >= 2: + print_str += colored(str, 'green') + elif line.startswith('-') and i >= 2: + print_str += colored(str, 'red') + else: + print_str += str + return print_str class Sha1Reader(object):