e0c8ce0915790a45d515045483bd81a42bd74e99
[jelmer/dulwich.git] / dulwich / porcelain.py
1 # porcelain.py -- Porcelain-like layer on top of Dulwich
2 # Copyright (C) 2013 Jelmer Vernooij <jelmer@samba.org>
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # or (at your option) a later version of the License.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
17 # MA  02110-1301, USA.
18
19 import os
20 import sys
21
22 from dulwich.client import get_transport_and_path
23 from dulwich.patch import write_tree_diff
24 from dulwich.repo import (BaseRepo, Repo)
25 from dulwich.server import update_server_info as server_update_server_info
26
27 """Simple wrapper that provides porcelain-like functions on top of Dulwich.
28
29 Currently implemented:
30  * archive
31  * add
32  * clone
33  * commit
34  * init
35  * remove
36  * update-server-info
37  * symbolic-ref
38
39 These functions are meant to behave similarly to the git subcommands.
40 Differences in behaviour are considered bugs.
41 """
42
43 __docformat__ = 'restructuredText'
44
45
46 def open_repo(path_or_repo):
47     """Open an argument that can be a repository or a path for a repository."""
48     if isinstance(path_or_repo, BaseRepo):
49         return path_or_repo
50     return Repo(path_or_repo)
51
52
53 def archive(location, committish=None, outstream=sys.stdout,
54             errstream=sys.stderr):
55     """Create an archive.
56
57     :param location: Location of repository for which to generate an archive.
58     :param committish: Commit SHA1 or ref to use
59     :param outstream: Output stream (defaults to stdout)
60     :param errstream: Error stream (defaults to stderr)
61     """
62
63     client, path = get_transport_and_path(location)
64     if committish is None:
65         committish = "HEAD"
66     client.archive(path, committish, outstream.write, errstream.write)
67
68
69 def update_server_info(repo="."):
70     """Update server info files for a repository.
71
72     :param repo: path to the repository
73     """
74     r = open_repo(repo)
75     server_update_server_info(r)
76
77
78 def symbolic_ref(repo, ref_name, force=False):
79     """Set git symbolic ref into HEAD.
80
81     :param repo: path to the repository
82     :param ref_name: short name of the new ref
83     :param force: force settings without checking if it exists in refs/heads
84     """
85     repo_obj = open_repo(repo)
86     ref_path = 'refs/heads/%s' % ref_name
87     if not force and ref_path not in repo_obj.refs.keys():
88         raise ValueError('fatal: ref `%s` is not a ref' % ref_name)
89     repo_obj.refs.set_symbolic_ref('HEAD', ref_path)
90
91
92 def commit(repo=".", message=None, author=None, committer=None):
93     """Create a new commit.
94
95     :param repo: Path to repository
96     :param message: Optional commit message
97     :param author: Optional author name and email
98     :param committer: Optional committer name and email
99     :return: SHA1 of the new commit
100     """
101     # FIXME: Support --all argument
102     # FIXME: Support --signoff argument
103     r = open_repo(repo)
104     return r.do_commit(message=message, author=author,
105         committer=committer)
106
107
108 def init(path=".", bare=False):
109     """Create a new git repository.
110
111     :param path: Path to repository.
112     :param bare: Whether to create a bare repository.
113     :return: A Repo instance
114     """
115     if not os.path.exists(path):
116         os.mkdir(path)
117
118     if bare:
119         return Repo.init_bare(path)
120     else:
121         return Repo.init(path)
122
123
124 def clone(source, target=None, bare=False, outstream=sys.stdout):
125     """Clone a local or remote git repository.
126
127     :param source: Path or URL for source repository
128     :param target: Path to target repository (optional)
129     :param bare: Whether or not to create a bare repository
130     :param outstream: Optional stream to write progress to
131     :return: The new repository
132     """
133     client, host_path = get_transport_and_path(source)
134
135     if target is None:
136         target = host_path.split("/")[-1]
137
138     if not os.path.exists(target):
139         os.mkdir(target)
140     if bare:
141         r = Repo.init_bare(target)
142     else:
143         r = Repo.init(target)
144     remote_refs = client.fetch(host_path, r,
145         determine_wants=r.object_store.determine_wants_all,
146         progress=outstream.write)
147     r["HEAD"] = remote_refs["HEAD"]
148     return r
149
150
151 def add(repo=".", paths=None):
152     """Add files to the staging area.
153
154     :param repo: Repository for the files
155     :param paths: Paths to add
156     """
157     # FIXME: Support patterns, directories, no argument.
158     r = open_repo(repo)
159     r.stage(paths)
160
161
162 def rm(repo=".", paths=None):
163     """Remove files from the staging area.
164
165     :param repo: Repository for the files
166     :param paths: Paths to remove
167     """
168     r = open_repo(repo)
169     index = r.open_index()
170     for p in paths:
171         del index[p]
172     index.write()
173
174
175 def print_commit(commit, outstream):
176     """Write a human-readable commit log entry.
177
178     :param commit: A `Commit` object
179     :param outstream: A stream file to write to
180     """
181     outstream.write("-" * 50 + "\n")
182     outstream.write("commit: %s\n" % commit.id)
183     if len(commit.parents) > 1:
184         outstream.write("merge: %s\n" % "...".join(commit.parents[1:]))
185     outstream.write("author: %s\n" % commit.author)
186     outstream.write("committer: %s\n" % commit.committer)
187     outstream.write("\n")
188     outstream.write(commit.message + "\n")
189     outstream.write("\n")
190
191
192 def log(repo=".", outstream=sys.stdout):
193     """Write commit logs.
194
195     :param repo: Path to repository
196     :param outstream: Stream to write log output to
197     """
198     r = open_repo(repo)
199     walker = r.get_walker()
200     for entry in walker:
201         print_commit(entry.commit, outstream)
202
203
204 def show(repo=".", committish=None, outstream=sys.stdout):
205     """Print the changes in a commit.
206
207     :param repo: Path to repository
208     :param committish: Commit to write
209     :param outstream: Stream to write to
210     """
211     if committish is None:
212         committish = "HEAD"
213     r = open_repo(repo)
214     commit = r[committish]
215     parent_commit = r[commit.parents[0]]
216     print_commit(commit, outstream)
217     write_tree_diff(outstream, r.object_store, parent_commit.tree, commit.tree)