@@ -3033,6 +3033,155 @@ tree object, generate and store the corresponding commit object, and
30333033update the HEAD branch to the new commit (remember: a branch is just a
30343034ref to a commit).
30353035
3036+ #+begin_src python :tangle libwyag.py
3037+ argsp = argsubparsers.add_parser("commit", help="Record changes to the repository.")
3038+
3039+ argsp.add_argument("-m",
3040+ metavar="message",
3041+ dest="message",
3042+ help="Message to associate with this commit.")
3043+ #+end_src
3044+
3045+ Before we get to the interesting details, we will need to read git's
3046+ config to get the name of the default user, which we'll use as the
3047+ author of commits. We'll use the same =configparser= library we've
3048+ used to read repo's config.
3049+
3050+ #+begin_src python :tangle libwyag.py
3051+ def gitconfig_read():
3052+ xdg_config_home = os.environ["XDG_CONFIG_HOME"] if "XDG_CONFIG_HOME" in os.environ else "~/.config"
3053+ configfiles = [
3054+ os.path.expanduser(os.path.join(xdg_config_home, "git/config")),
3055+ os.path.expanduser("~/.gitconfig")
3056+ ]
3057+
3058+ config = configparser.ConfigParser()
3059+ config.read(configfiles)
3060+ return config
3061+ #+end_src
3062+
3063+ And just a simple function to grab, and format, the user identity:
3064+
3065+ #+begin_src python :tangle libwyag.py
3066+ def gitconfig_user_get(config):
3067+ if "user" in config:
3068+ if "name" in config["user"] and "email" in config["user"]:
3069+ return "{} <{}>".format(config["user"]["name"], config["user"]["email"])
3070+ return None
3071+ #+end_src
3072+
3073+ Now for the interesting part. We first need to build a tree from the
3074+ index. This isn't hard, but notice that while the index is flat (it
3075+ stores full paths for the whole worktree), a tree is a recursive
3076+ structure. What we do is start from the deepest directory, build a
3077+ tree with its contents, write that tree to the repository, add a
3078+ reference to that tree to its parent, and resume at upper levels until
3079+ we hit home. The /last/ tree we'll have built is the only one we'll
3080+ need in the commit, since it's the root and references all others ---
3081+ so this function will simply return its hash.
3082+
3083+ #+begin_src python :tangle libwyag.py
3084+ def tree_from_index(repo, index):
3085+ contents = dict()
3086+ contents[""] = list()
3087+
3088+ # Enumerate entries, and turn them into a dictionary where keys
3089+ # are directories, and values are lists of directory contents.
3090+ for entry in index.entries:
3091+ dirname = os.path.dirname(entry.name)
3092+
3093+ # We create all dictonary entries up to root (""). We need
3094+ # them *all*, because even if a directory holds no files it
3095+ # will contain at least a tree.
3096+ key = dirname
3097+ while key != "":
3098+ if not key in contents:
3099+ contents[key] = list()
3100+ key = os.path.dirname(key)
3101+
3102+ # For now, simply store the entry in the list.
3103+ contents[dirname].append(entry)
3104+
3105+ # Get keys (= directories) and sort them by length, descending.
3106+ # easiest way to traverse the list of paths bottom-up. This means
3107+ # that we'll always encounter a given path before its parent,
3108+ # which is all we need.
3109+ sorted_paths = sorted(contents.keys(), key=len, reverse=True)
3110+ sha = None
3111+
3112+ # We create, and write, trees
3113+ for path in sorted_paths:
3114+ tree = GitTree()
3115+
3116+ for entry in contents[path]:
3117+ if isinstance(entry, GitIndexEntry):
3118+ # We transcode the mode: the entry stores it as integers,
3119+ # we need an octal ASCII representation for the tree.
3120+ leaf_mode = "{:02o}{:04o}".format(entry.mode_type, entry.mode_perms).encode("ascii")
3121+ leaf = GitTreeLeaf(mode = leaf_mode, path=os.path.basename(entry.name), sha=entry.sha)
3122+ else:
3123+ leaf = GitTreeLeaf(mode = b"040000", path=entry[0], sha=entry[1])
3124+
3125+ tree.items.append(leaf)
3126+
3127+ # Write the new tree object.
3128+ sha = object_write(tree, repo)
3129+
3130+ # Add the new tree to its parent.
3131+ parent = os.path.dirname(path)
3132+ base = os.path.basename(path)
3133+ contents[parent].append((base, sha))
3134+
3135+ return sha
3136+ #+end_src
3137+
3138+ The function to create a commit object is simple enough, it just takes a few arguments.
3139+
3140+ #+begin_src python :tangle libwyag.py
3141+ def commit_create(repo, tree, parent, author, timestamp, tz, message):
3142+ commit = GitCommit()
3143+ commit.kvlm[b"tree"] = tree.encode("ascii")
3144+ if parent:
3145+ commit.kvlm[b"parent"] = parent.encode("utf8")
3146+
3147+ author = author + timestamp.strftime(" %s ") + tz
3148+
3149+ commit.kvlm[b"author"] = author.encode("utf8")
3150+ commit.kvlm[b"committer"] = author.encode("utf8")
3151+ commit.kvlm[b''] = message.encode("utf8")
3152+
3153+ return object_write(commit, repo)
3154+ #+end_src
3155+
3156+ And finally, the actual =cmd_commit=, the bridge to the =wyag commit= command:
3157+
3158+ #+begin_src python :tangle libwyag.py
3159+ def cmd_commit(args):
3160+ print(args)
3161+ repo = repo_find()
3162+ index = index_read(repo)
3163+ # Create trees, grab back SHA for the root tree.
3164+ tree = tree_from_index(repo, index)
3165+
3166+ # Create the commit object itself
3167+ commit = commit_create(repo,
3168+ tree,
3169+ object_find(repo, "HEAD"),
3170+ gitconfig_user_get(gitconfig_read()),
3171+ datetime.now(),
3172+ "+0200", # @FIXME
3173+ args.message)
3174+
3175+ # Update HEAD
3176+ active_branch = branch_get_active(repo)
3177+ if active_branch: # If we're on a branch, we update refs/heads/BRANCH
3178+ with open(repo_file(repo, os.path.join("refs/heads", active_branch)), "w") as fd:
3179+ fd.write(commit + "\n")
3180+ else: # Otherwise, we update HEAD itself.
3181+ with open(repo_file(repo, "HEAD"), "w") as fd:
3182+ fd.write("\n")
3183+ #+end_src
3184+
30363185* Final words
30373186
30383187** Comments, feedback and issues
0 commit comments