今天我们讲讲gerrit 的 ssh 命令的 ls-user-refs命令的实现的实现,采用的代码是gerrit的 stable2.15 分支的,因为我们使用的版本是 2.15.1的。

LsUserRefs 类详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
这个命令在 com.google.gerrit.sshd.commands.LsUserRefs 类中,
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@CommandMetaData(
name = "ls-user-refs",
description = "List refs visible to a specific user",
runsAt = MASTER_OR_SLAVE)
public class LsUserRefs extends SshCommand {
@Inject private AccountResolver accountResolver;
@Inject private OneOffRequestContext requestContext;
@Inject private VisibleRefFilter.Factory refFilterFactory;
@Inject private GitRepositoryManager repoManager;

@Option(
name = "--project",
aliases = {"-p"},
metaVar = "PROJECT",
required = true,
usage = "project for which the refs should be listed")
private ProjectControl projectControl;

@Option(
name = "--user",
aliases = {"-u"},
metaVar = "USER",
required = true,
usage = "user for which the groups should be listed")
private String userName;

@Option(name = "--only-refs-heads", usage = "list only refs under refs/heads")
private boolean onlyRefsHeads;

@Override
protected void run() throws Failure {
Account userAccount;
try {
userAccount = accountResolver.find(userName);
} catch (OrmException | IOException | ConfigInvalidException e) {
throw die(e);
}
if (userAccount == null) {
stdout.print("No single user could be found when searching for: " + userName + '\n');
stdout.flush();
return;
}

Project.NameKey projectName = projectControl.getProject().getNameKey();
try (Repository repo = repoManager.openRepository(projectName);
ManualRequestContext ctx = requestContext.openAs(userAccount.getId())) {
try {
Map<String, Ref> refsMap =
refFilterFactory
.create(projectControl.getProjectState(), repo)
.filter(repo.getRefDatabase().getRefs(ALL), false);

for (String ref : refsMap.keySet()) {
if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) {
stdout.println(ref);
}
}
} catch (IOException e) {
throw new Failure(1, "fatal: Error reading refs: '" + projectName, e);
}
} catch (RepositoryNotFoundException e) {
throw die("'" + projectName + "': not a git archive");
} catch (IOException | OrmException e) {
throw die("Error opening: '" + projectName);
}
}
}

注解@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
这个暂时没理解, 估计是 需要什么 能力, 也就是 Requires Capability字面翻译, 需要一个全局的 能力GlobalCapability, 感觉像是 需要什么权限类似的,就是说执行这个 ls-user-refs命令 命令是需要一定权限的。

下面我们看看 com.google.gerrit.common.data.GlobalCapability 类里面的, 可以发现 里面确实是一个一个的权限类型。
/** Server wide capabilities. Represented as {@link Permission} objects. */
public class GlobalCapability {
/** Ability to access the database (with gsql). */
public static final String ACCESS_DATABASE = "accessDatabase";

/**
* Denotes the server's administrators.
*
* <p>This is similar to UNIX root, or Windows SYSTEM account. Any user that has this capability
* can perform almost any other action, or can grant themselves the power to perform any other
* action on the site. Most of the other capabilities and permissions fall-back to the predicate
* "OR user has capability ADMINISTRATE_SERVER".
*/
public static final String ADMINISTRATE_SERVER = "administrateServer";

GlobalCapability.ADMINISTRATE_SERVER 就是管理员。

@RequiresCapability 这个注解 放这里就是说明 执行 ls-user-refs命令 需要 administrateServer 权限。

注解@CommandMetaData 分析

1
2
3
4
5
6
7
@CommandMetaData(
name = "ls-user-refs",
description = "List refs visible to a specific user",
runsAt = MASTER_OR_SLAVE)

这个是 定义 这个类是要执行什么命令的, name 就是命令名称了。 description 命令的描述,runsAt 表示这个命令可以运行在master端 还是 slave端。(gerrit也有 master - slave的模式的。有些命令是在slave模式下面没有的。)

LsUserRefs extends SshCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LsUserRefs extends SshCommand {
@Inject private AccountResolver accountResolver;
@Inject private OneOffRequestContext requestContext;
@Inject private VisibleRefFilter.Factory refFilterFactory;
@Inject private GitRepositoryManager repoManager;

从这个 extends 继承关系可以发现 所有的 ssh -p 29418 gerrit.example.com gerrit <命令> 这样类似的命令估计都会 继承 SshCommand 类。
一般的类似spring IOC这样的容器,在服务启动之后 会扫描所有的 子类的实现, 例如扫描所有 SshCommand 子类,
然后就可以得到所有的ssh命令行的实现,然后在自动的依赖注入,下面我们就会看到。
又比如,jenkins中,很多类都会实现 JobProperty ,(比如这个插件 public class BuildLogCompressor extends JobProperty<AbstractProject<?, ?>>),
这样jenkins在启动的时候会 扫描所有的 实现JobProperty类。



@Inject依赖注入

1
2
3
4
5
6
7
8
9
10
public class LsUserRefs extends SshCommand {
@Inject private AccountResolver accountResolver;
@Inject private OneOffRequestContext requestContext;
@Inject private VisibleRefFilter.Factory refFilterFactory;
@Inject private GitRepositoryManager repoManager;

从这里 @Inject 我们可以看到 这里有4个成员, 这几个的实例化估计都是 通过 @Inject ,或者说是某个框架自动注入的。
gerrit应该用的不是我们平常属性的spring框架,是google自己开发的一个 Guice - 轻量级IoC容器,Google公司开发的轻量级IoC容器,其特点是: 速度快,据说是spring的100倍速度;无需配置文件。


@Option 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
  @Option(
name = "--project",
aliases = {"-p"},
metaVar = "PROJECT",
required = true,
usage = "project for which the refs should be listed")
private ProjectControl projectControl;

@Option(
name = "--user",
aliases = {"-u"},
metaVar = "USER",
required = true,
usage = "user for which the groups should be listed")
private String userName;

@Option(name = "--only-refs-heads", usage = "list only refs under refs/heads")
private boolean onlyRefsHeads;

先看 private ProjectControl projectControl;
这个注解 应该是 设置命令 需要的选项,然后把 命令行参数 选项后面的值 初始化到对应的类成员变量中,
例如 --project 这个选项,如果命令行上 填写了, --project git/android/repo 这样的, gerrit就会初始 projectControl 这个,
这个变量是个复杂的类型, 不过这个类 ProjectControl 里面肯定有一个属性 会存放 git/android/repo 这个值的。不过通过看 ProjectControl源码发现,
并没有这么简单,里面嵌入了好几层。 不过通过字面意思和命令参数的作用,可以猜出 这个 projectControl 就是存放 git 仓库路径的, 就是gerrit上的project的。 例如命令 create-branch 中有这个 project的概念。
下面是 create-branch 中的
com.google.gerrit.sshd.commands.CreateBranchCommand
@Argument(index = 0, required = true, metaVar = "PROJECT", usage = "name of the project")
private ProjectControl project;
这个 ProjectControl 属性,和我们猜测的一样。

name = "--project", 指定选项名称,
aliases = {"-p"}, 指定了选项别名,这里是一个短选项,例如很多linux中的命令 都有 长选项 和 短选项的。
metaVar = "PROJECT", 说明选项后面需要跟上一个值的。
required = true, 表面这个选项是必须的。有些选项是可选的,就是可以不用显示的指出,这些应该是会有一个默认值。
但是像这个 如果不执行,怎么知道你要查看哪个仓库下面的 refs 是否有权限呢??? 要不然所有仓库都列出来?,这样就会太多了。
一个命令 尽可能的做到功能单一, 一个命令再和其他命令组合起来实现一个复杂的命令。就比如这个想列出来所有仓库的,
可以先用 ls-projects 命令列出所有仓库,然后再结合 这个 ls-user-refs 命令。这就是unix编程艺术 思想。
usage = "project for which the refs should be listed" 这个说明了这个选项的作用,描述等等。


再看另外一个 private String userName;
这个应该是 用户名,就是你想列出 哪个用的 对这个仓库的

最后看 private boolean onlyRefsHeads;
这是一个布尔类型的,如果命令行有个 --only-refs-heads 选项,就只会 列出 refs/heads/ 下面的。也就是之后列出分支了。
如果没有就会 列出 refs/heads/ 和 refs/tags/ 下面的。

这个注解 其实是来自jenkins的。哈哈哈。 import org.kohsuke.args4j.Option;

run() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  @Override
protected void run() throws Failure {

最后我们看这个 run() 方法,这个很类似 java 中Thread的用法,里面放到就是命令主要实现。

public abstract class SshCommand extends BaseCommand {
SshCommand 继承 BaseCommand 类,估计出了 ssh 命令,还会有其他的命令类型吧。 例如 HttpCommand??? GitCommand???这里只是猜测。。

public abstract class BaseCommand implements Command {
BaseCommand 又实现了 接口 Command, 原来是 org.apache.sshd.server.Command 这个接口,也就是 apache 的ssh命令实现。

我们猜测的 真对,还真有个 gitcommand,估计是执行git相关的命令的。
public abstract class AbstractGitCommand extends BaseCommand


从run() 方法中我们可以看到 仓库路径 project 是怎么用的:
Project.NameKey projectName = projectControl.getProject().getNameKey();
仓库名,仓库路径是存放为 Project.NameKey 类型的。。。。。projectControl里面包装了好几层。。。
有了git仓库 然后怎么使用呢,调用 repoManager 来 处理 git仓库:
try (Repository repo = repoManager.openRepository(projectName);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from https://blog.csdn.net/czq7511/article/details/73610510
本文章主要记录本人在学习开发Gerrit插件过程中的一些心得,一些零零碎碎的资料:

一、关于插件jar的META-INF/MANIFEST.MF文件:这个文件的主要作用是记录一些有关这个jar的属性,以便被识别、引用或加载,也可以理解为是这个jar的配置文件。

这里举些例子:

Manifest-Version: 1.0----配置版本号。如果日后要做升级兼容的话,可以利用这个属性。
Gerrit-ApiType: plugin----指定Gerrit插件类型为plugin。Gerrit插件类型有两种:plugin和extension
Gerrit-ApiVersion: 2.13.7----指定该插件匹配的Gerrit平台版本,如果指定版本不一致的话,插件很有可能不能用。
Implementation-Title: Cookbook plugin ----实现Titile
Implementation-Version: 2.13.7----实现的版本号,也就是该插件的版本号,一般和要适应的Gerrit平台的版本号一致。
Archiver-Version: Plexus Archiver----
Built-By: czq ----打包作者

Gerrit-PluginName: cookbook----插件名称
Gerrit-Module: com.googlesource.gerrit.plugins.cookbook.Module----Gerrit加载插件时,会根据这个指定去加载相应的Module类。该类主要实现前后台业务逻辑。
Gerrit-HttpModule: com.googlesource.gerrit.plugins.cookbook.HttpModule ----Gerrit加载插件时,会根据这个指定去加载相应的HttpModule类。该类主要实现Http接口业务逻辑。
Gerrit-SshModule: com.googlesource.gerrit.plugins.cookbook.SshModule ----Gerrit加载插件时,会根据这个指定去加载相应的HttpModule类。该类主要实现SSH接口业务逻辑。
Implementation-Vendor: Gerrit Code Review----开发商
Created-By: Apache Maven 3.5.0----编译打包工具
Build-Jdk: 1.8.0_101----编译的JDK版本
Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/cookbook-plugin ----代码实现URL

二、如何编译插件请参考:http://blog.csdn.net/czq7511/article/details/73189168

三、Gerrit插件的前端独立UI主要是用GWT来开发,通过在pom.xml里来指定编译入口,如(module是入口类,省去了扩展名gwt.xml):

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>gwt-maven-plugin</artifactId>
<version>2.7.0</version>
<configuration>
<module>com.googlesource.gerrit.plugins.cookbook.HelloForm</module>
<disableClassMetadata>true</disableClassMetadata>
<disableCastChecking>true</disableCastChecking>
<webappDirectory>${project.build.directory}/classes/static</webappDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>

GWT编译时,会默认对与gwt.xml文件同目录下的client和public两目录下的所以文件进行编译打包,包的名字默认是插件名,可以修改,可以在gwt.xml里的rename-to属性上指定。该包名主要用于gerrit加载插件时,Module模块里指定加载,以便加载前端业务,具体加载代码:

DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new GwtPlugin("rename-to属性上指定的名字"));

四、对插件创建的screen的引用,必须加 #/x/,因为screen被加载时,会在指定的token前加上/x/ 标签,可参见加载源码:

gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/plugin.java 里的注释说明



五、使用MenuItem时,第二个参当我若有 # 号,第三个参数要填写,且为空字符,否则 # 号在前端会被显示为 %23

六、如何判断用户是否已登录:

  1、菜单:继承于菜单TopMenu.class的类有一个构建方法,构建时会传入用户信息类,可通过该类来判断,如:

@Inject
public ImporterTopMenu(@PluginName String pluginName, Provider<CurrentUser> userProvider) {
if (userProvider.get().isIdentifiedUser()) {
//这里表示用户已登录
} else {
//这里表示用户未登录
}
}    



  2、服务:服务类是继承于RestModifyView.class的,该类的构建方法也会传入 用户信息类 参数,用法同 1