Skip to content

Commit

Permalink
First draft of commit section (commit created from WYAG)
Browse files Browse the repository at this point in the history
  • Loading branch information
thblt committed Jul 27, 2023
1 parent ef5e90c commit ed26daf
Showing 1 changed file with 149 additions and 0 deletions.
149 changes: 149 additions & 0 deletions write-yourself-a-git.org
Original file line number Diff line number Diff line change
Expand Up @@ -3033,6 +3033,155 @@ tree object, generate and store the corresponding commit object, and
update the HEAD branch to the new commit (remember: a branch is just a
ref to a commit).

#+begin_src python :tangle libwyag.py
argsp = argsubparsers.add_parser("commit", help="Record changes to the repository.")

argsp.add_argument("-m",
metavar="message",
dest="message",
help="Message to associate with this commit.")
#+end_src

Before we get to the interesting details, we will need to read git's
config to get the name of the default user, which we'll use as the
author of commits. We'll use the same =configparser= library we've
used to read repo's config.

#+begin_src python :tangle libwyag.py
def gitconfig_read():
xdg_config_home = os.environ["XDG_CONFIG_HOME"] if "XDG_CONFIG_HOME" in os.environ else "~/.config"
configfiles = [
os.path.expanduser(os.path.join(xdg_config_home, "git/config")),
os.path.expanduser("~/.gitconfig")
]

config = configparser.ConfigParser()
config.read(configfiles)
return config
#+end_src

And just a simple function to grab, and format, the user identity:

#+begin_src python :tangle libwyag.py
def gitconfig_user_get(config):
if "user" in config:
if "name" in config["user"] and "email" in config["user"]:
return "{} <{}>".format(config["user"]["name"], config["user"]["email"])
return None
#+end_src

Now for the interesting part. We first need to build a tree from the
index. This isn't hard, but notice that while the index is flat (it
stores full paths for the whole worktree), a tree is a recursive
structure. What we do is start from the deepest directory, build a
tree with its contents, write that tree to the repository, add a
reference to that tree to its parent, and resume at upper levels until
we hit home. The /last/ tree we'll have built is the only one we'll
need in the commit, since it's the root and references all others ---
so this function will simply return its hash.

#+begin_src python :tangle libwyag.py
def tree_from_index(repo, index):
contents = dict()
contents[""] = list()

# Enumerate entries, and turn them into a dictionary where keys
# are directories, and values are lists of directory contents.
for entry in index.entries:
dirname = os.path.dirname(entry.name)

# We create all dictonary entries up to root (""). We need
# them *all*, because even if a directory holds no files it
# will contain at least a tree.
key = dirname
while key != "":
if not key in contents:
contents[key] = list()
key = os.path.dirname(key)

# For now, simply store the entry in the list.
contents[dirname].append(entry)

# Get keys (= directories) and sort them by length, descending.
# easiest way to traverse the list of paths bottom-up. This means
# that we'll always encounter a given path before its parent,
# which is all we need.
sorted_paths = sorted(contents.keys(), key=len, reverse=True)
sha = None

# We create, and write, trees
for path in sorted_paths:
tree = GitTree()

for entry in contents[path]:
if isinstance(entry, GitIndexEntry):
# We transcode the mode: the entry stores it as integers,
# we need an octal ASCII representation for the tree.
leaf_mode = "{:02o}{:04o}".format(entry.mode_type, entry.mode_perms).encode("ascii")
leaf = GitTreeLeaf(mode = leaf_mode, path=os.path.basename(entry.name), sha=entry.sha)
else:
leaf = GitTreeLeaf(mode = b"040000", path=entry[0], sha=entry[1])

tree.items.append(leaf)

# Write the new tree object.
sha = object_write(tree, repo)

# Add the new tree to its parent.
parent = os.path.dirname(path)
base = os.path.basename(path)
contents[parent].append((base, sha))

return sha
#+end_src

The function to create a commit object is simple enough, it just takes a few arguments.

#+begin_src python :tangle libwyag.py
def commit_create(repo, tree, parent, author, timestamp, tz, message):
commit = GitCommit()
commit.kvlm[b"tree"] = tree.encode("ascii")
if parent:
commit.kvlm[b"parent"] = parent.encode("utf8")

author = author + timestamp.strftime(" %s ") + tz

commit.kvlm[b"author"] = author.encode("utf8")
commit.kvlm[b"committer"] = author.encode("utf8")
commit.kvlm[b''] = message.encode("utf8")

return object_write(commit, repo)
#+end_src

And finally, the actual =cmd_commit=, the bridge to the =wyag commit= command:

#+begin_src python :tangle libwyag.py
def cmd_commit(args):
print(args)
repo = repo_find()
index = index_read(repo)
# Create trees, grab back SHA for the root tree.
tree = tree_from_index(repo, index)

# Create the commit object itself
commit = commit_create(repo,
tree,
object_find(repo, "HEAD"),
gitconfig_user_get(gitconfig_read()),
datetime.now(),
"+0200", # @FIXME
args.message)

# Update HEAD
active_branch = branch_get_active(repo)
if active_branch: # If we're on a branch, we update refs/heads/BRANCH
with open(repo_file(repo, os.path.join("refs/heads", active_branch)), "w") as fd:
fd.write(commit + "\n")
else: # Otherwise, we update HEAD itself.
with open(repo_file(repo, "HEAD"), "w") as fd:
fd.write("\n")
#+end_src

* Final words

** Comments, feedback and issues
Expand Down

2 comments on commit ed26daf

@rogudator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

legend

@CubeFlix
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we came full circle with this one

Please sign in to comment.