diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..7bd0a14 Binary files /dev/null and b/.DS_Store differ diff --git a/pit-project/.DS_Store b/pit-project/.DS_Store new file mode 100644 index 0000000..736414b Binary files /dev/null and b/pit-project/.DS_Store differ diff --git a/pit-project/commands/__init__.py b/pit-project/commands/__init__.py index bc8e1ee..de9add8 100644 --- a/pit-project/commands/__init__.py +++ b/pit-project/commands/__init__.py @@ -19,3 +19,4 @@ # from . import pull # from . import clone from . import stash +from . import tag diff --git a/pit-project/commands/checkout.py b/pit-project/commands/checkout.py index 0016776..46d7530 100644 --- a/pit-project/commands/checkout.py +++ b/pit-project/commands/checkout.py @@ -36,7 +36,11 @@ def run(args): elif len(targets) == 1 and _is_branch(repo_root, targets[0]): handle_branch_checkout(repo_root, targets[0]) - # Case 3: checkout ... + # Case 3: checkout + elif len(targets) == 1 and _is_tag(repo_root, targets[0]): + handle_tag_checkout(repo_root, targets[0]) + + # Case 4: checkout ... else: handle_file_restore(repo_root, targets) @@ -44,6 +48,11 @@ def _is_branch(repo_root, name): branches = repository.get_all_branches(repo_root) return name in branches +def _is_tag(repo_root, name): + tags = repository.get_all_tags(repo_root) + return name in tags + + def handle_create_and_checkout(repo_root, branch_name): # 1. Check if branch already exists if _is_branch(repo_root, branch_name): @@ -71,6 +80,42 @@ def handle_branch_checkout(repo_root, branch_name): perform_checkout(repo_root, branch_name) + + +def handle_tag_checkout(repo_root, tag_name): + # Similar to branch checkout, but results in detached HEAD + # 1. Validate clean + if not is_clean(repo_root): + print("error: Your local changes would be overwritten by checkout.", file=sys.stderr) + sys.exit(1) + + # 2. Get commit + commit_hash = repository.get_tag_commit(repo_root, tag_name) + if not commit_hash: + print(f"fatal: tag '{tag_name}' not found.", file=sys.stderr) + sys.exit(1) + + # 3. Update Workdir & Index + # Reuse perform_checkout logic's parts + target_files = objects.get_commit_files(repo_root, commit_hash) + current_commit = repository.get_head_commit(repo_root) + # If in detached HEAD or initial state + current_files = objects.get_commit_files(repo_root, current_commit) if current_commit else {} + + update_working_directory(repo_root, current_files, target_files) + update_index(repo_root, target_files) + + # 4. Detached HEAD + head_path = os.path.join(repo_root, '.pit', 'HEAD') + with open(head_path, 'w') as f: + f.write(commit_hash) + + print(f"Note: checking out '{tag_name}'.") + print("\nYou are in 'detached HEAD' state. You can look around, make experimental") + print("changes and commit them, and you can discard any commits you make in this") + print("state without impacting any branches by switching back to a branch.") + print(f"\nHEAD is now at {commit_hash[:7]}") + def perform_checkout(repo_root, target_branch): # 1. Validate clean state if not is_clean(repo_root): @@ -154,13 +199,23 @@ def is_clean(repo_root): return True def load_index(repo_root): + index_path = os.path.join(repo_root, '.pit', 'index') index_files = {} if os.path.exists(index_path): with open(index_path, 'r') as f: for line in f: - hash_val, path = line.strip().split(' ', 1) - index_files[path] = hash_val + parts = line.strip().split(' ') + if len(parts) >= 4: + # New format: hash mtime size path + hash_val = parts[0] + # mtime = parts[1], size = parts[2] - not used here + path = " ".join(parts[3:]) + index_files[path] = hash_val + else: + # Old format + hash_val, path = line.strip().split(' ', 1) + index_files[path] = hash_val return index_files def update_working_directory(repo_root, current_files, target_files): diff --git a/pit-project/commands/tag.py b/pit-project/commands/tag.py new file mode 100644 index 0000000..7fa8ddb --- /dev/null +++ b/pit-project/commands/tag.py @@ -0,0 +1,51 @@ +# The command: pit tag [] +# What it does: Creates a lightweight tag ref pointing to the current HEAD, or lists existing tags. +# How it does: To create a tag, it gets the HEAD commit hash and writes it to a file in `.pit/refs/tags/`. To list, it reads that directory. +# What data structure it uses: Files references (similar to branches). + +import os +import sys +from utils import repository + +def run(args): + repo_root = repository.find_repo_root() + if not repo_root: + print("fatal: not a pit repository", file=sys.stderr) + sys.exit(1) + + if args.name: + create_tag(repo_root, args.name) + else: + list_tags(repo_root) + +def create_tag(repo_root, name): + # Validate tag name? (Simple check for now) + if not name or '/' in name or '\\' in name or name.startswith('.'): + print(f"fatal: Invalid tag name '{name}'", file=sys.stderr) + sys.exit(1) + + head_commit = repository.get_head_commit(repo_root) + if not head_commit: + print("fatal: Failed to resolve 'HEAD' as a valid revision.", file=sys.stderr) + sys.exit(1) + + tags_dir = os.path.join(repo_root, '.pit', 'refs', 'tags') + os.makedirs(tags_dir, exist_ok=True) + + tag_path = os.path.join(tags_dir, name) + if os.path.exists(tag_path): + print(f"fatal: tag '{name}' already exists", file=sys.stderr) + sys.exit(1) + + with open(tag_path, 'w') as f: + f.write(head_commit) + + print(f"Created tag '{name}' at {head_commit[:7]}") + +def list_tags(repo_root): + tags = repository.get_all_tags(repo_root) + if not tags: + return + + for tag in sorted(tags): + print(tag) diff --git a/pit-project/pit.py b/pit-project/pit.py index 8b964c9..0191921 100644 --- a/pit-project/pit.py +++ b/pit-project/pit.py @@ -2,7 +2,7 @@ from commands import ( init, add, commit, log, status, config, branch, checkout, diff, merge, reset, - revert, clean, + revert, clean, stash, tag # remote, push, pull, clone ) @@ -87,6 +87,12 @@ def main(): clean_parser.add_argument("-d", action="store_true", help="Remove untracked directories as well.") clean_parser.set_defaults(func=clean.run) + # Command: stash + stash_parser = subparsers.add_parser("stash", help="Stash the changes in a dirty working directory away.") + stash_parser.add_argument("action", nargs="?", choices=["push", "list", "pop", "show", "drop", "clear"], default="push", help="The action to perform (push, list, pop, show, drop, clear)") + stash_parser.add_argument("stash_arg", nargs="?", help="Stash index (e.g. stash@{0}) or message for push") + stash_parser.set_defaults(func=stash.run) + # # Command: remote # remote_parser = subparsers.add_parser("remote", help="Manage remote repositories (HTTPS only)") # remote_parser.add_argument("subcommand", help="Subcommand: add, remove, list, set-url") @@ -99,9 +105,9 @@ def main(): # push_parser.add_argument("remote", help="Remote name") # push_parser.add_argument("branch", help="Branch to push") # push_parser.add_argument("-u", "--set-upstream", action="store_true", - # # help="Set upstream branch tracking") + # help="Set upstream branch tracking") # push_parser.add_argument("-f", "--force", action="store_true", - # # help="Force push (overwrite remote)") + # help="Force push (overwrite remote)") # push_parser.set_defaults(func=push.run) # # Command: pull @@ -116,6 +122,11 @@ def main(): # clone_parser.add_argument("directory", nargs="?", help="The name of the directory to clone into.") # clone_parser.set_defaults(func=clone.run) + # Command: tag + tag_parser = subparsers.add_parser("tag", help="Create, list, delete or verify a tag object signed with GPG.") + tag_parser.add_argument("name", nargs="?", help="The name of the tag to create.") + tag_parser.set_defaults(func=tag.run) + # Parse the arguments args = parser.parse_args() diff --git a/pit-project/utils/config.py b/pit-project/utils/config.py index 1edee68..e6e6661 100644 --- a/pit-project/utils/config.py +++ b/pit-project/utils/config.py @@ -43,8 +43,8 @@ def write_config(key, value): # Sets a configuration key to a value and writes i raise FileNotFoundError("Not a Pit repository.") config_path = get_config_path(repo_root) -os.makedirs(os.path.dirname(config_path), exist_ok=True) -config = configparser.ConfigParser() + os.makedirs(os.path.dirname(config_path), exist_ok=True) + config = configparser.ConfigParser() if os.path.exists(config_path): config.read(config_path) diff --git a/pit-project/utils/repository.py b/pit-project/utils/repository.py index c5427fa..569f0e9 100644 --- a/pit-project/utils/repository.py +++ b/pit-project/utils/repository.py @@ -61,4 +61,17 @@ def get_branch_commit(repo_root, branch_name): # Retrieves the commit hash that if not os.path.exists(branch_path): return None with open(branch_path, 'r') as f: + return f.read().strip() + +def get_all_tags(repo_root): # Lists all tag names by reading the refs/tags directory + tags_dir = os.path.join(repo_root, '.pit', 'refs', 'tags') + if not os.path.isdir(tags_dir): + return [] + return [name for name in os.listdir(tags_dir) if not name.startswith('.')] + +def get_tag_commit(repo_root, tag_name): # Retrieves the commit hash that a given tag points to + tag_path = os.path.join(repo_root, '.pit', 'refs', 'tags', tag_name) + if not os.path.exists(tag_path): + return None + with open(tag_path, 'r') as f: return f.read().strip() \ No newline at end of file