diff --git a/libwyag.py b/libwyag.py index c8662c0..438f6ac 100644 --- a/libwyag.py +++ b/libwyag.py @@ -1064,8 +1064,136 @@ def check_ignore(rules, path): if os.path.isabs(path): raise Exception("This function requires path to be relative to the repository's root") + # Eh, just hardcode it + if (path.startswith(".git")): + return True + result = check_ignore_scoped(rules.scoped, path) if result != None: return result return check_ignore_absolute(rules.absolute, path) + +argsp = argsubparsers.add_parser("status", help="Show the working tree status.") + +def cmd_status(_): + repo = repo_find() + index = index_read(repo) + + cmd_status_branch(repo) + cmd_status_head_index(repo, index) + print() + cmd_status_index_worktree(repo, index) + +def branch_get_active(repo): + with open(GitRepository.repo_file(repo, "HEAD"), "r") as f: + head = f.read() + + if head.startswith("ref: refs/heads/"): + return(head[16:-1]) + else: + return False + +def cmd_status_branch(repo): + branch = branch_get_active(repo) + if branch: + print(f"On branch {branch}.") + else: + print("HEAD detached at {}".format(object_find(repo, "HEAD"))) + +def tree_to_dict(repo, ref, prefix=""): + ret = dict() + tree_sha = object_find(repo, ref, fmt=b"tree") + tree = object_read(repo, tree_sha) + + for leaf in tree.items: + full_path = join_path(prefix, leaf.path) + + # We read the object to extract its type (this is uselessly + # expensive: we could just open it as a file and read the + # first few bytes) + is_subtree = leaf.mode.startswith(b'04') + + # Depending on the type, we either store the path (if it's a + # blob, so a regular file), or recurse (if it's another tree, + # so a subdir) + if is_subtree: + ret.update(tree_to_dict(repo, leaf.sha, full_path)) + else: + ret[full_path] = leaf.sha + + return ret + +def cmd_status_head_index(repo, index): + print("Changes to be commited:") + + head = tree_to_dict(repo, "HEAD") + for entry in index.entries: + if entry.name in head: + if head[entry.name] != entry.sha: + print(" modified:", entry.name) + del head[entry.name] + else: + print(" added: ", entry.name) + + # Keys still in HEAD are files that we haven't met in the index, + # and thus have been deleted + for entry in head.keys(): + print(" deleted: ", entry) + +def cmd_status_index_worktree(repo, index): + print("Changes not staged for commit:") + + ignore = gitignore_read(repo) + + gitdir_prefix = repo.gitdir + "/" + + all_files = list() + + # We begin by walking the filesystem + for (root, _, files) in os.walk(repo.worktree, True): + if root==repo.gitdir or root.startswith(gitdir_prefix): + continue + for f in files: + full_path = join_path(root, f) + rel_path = os.path.relpath(full_path, repo.worktree).replace("\\", "/") + all_files.append(rel_path) + + # We now traverse the index, and compare real files with the cached + # versions. + + for entry in index.entries: + full_path = join_path(repo.worktree, entry.name) + + # That file *name* is in the index + + if not os.path.exists(full_path): + print(" deleted: ", entry.name) + else: + stat = os.stat(full_path) + + # Compare metadata + ctime_ns = entry.ctime[0] * 10**9 + entry.ctime[1] + mtime_ns = entry.mtime[0] * 10**9 + entry.mtime[1] + if (stat.st_ctime_ns != ctime_ns) or (stat.st_mtime_ns != mtime_ns): + # If different, deep compare. + # @FIXME This *will* crash on symlinks to dir. + with open(full_path, "rb") as fd: + new_sha = object_hash(fd, b"blob", None) + # If the hashes are the same, the files are actually the same. + same = entry.sha == new_sha + + if not same: + print(" modified:", entry.name) + + if entry.name in all_files: + all_files.remove(entry.name) + + print() + print("Untracked files:") + + for f in all_files: + # @TODO If a full directory is untracked, we should display + # its name without its contents. + if not check_ignore(ignore, f): + print(" ", f)